diff --git a/.rubocop.yml b/.rubocop.yml index 33c33fa..3aaa696 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,534 +1,512 @@ --- require: - rubocop-performance - rubocop-rspec AllCops: DisplayCopNames: true TargetRubyVersion: '2.4' Include: - "**/*.rb" Exclude: - bin/* - ".vendor/**/*" - "**/Gemfile" - "**/Rakefile" - pkg/**/* - spec/fixtures/**/* - vendor/**/* - "**/Puppetfile" - "**/Vagrantfile" - "**/Guardfile" Layout/LineLength: Description: People have wide screens, use them. Max: 200 RSpec/BeforeAfterAll: Description: Beware of using after(:all) as it may cause state to leak between tests. A necessary evil in acceptance testing. Exclude: - spec/acceptance/**/*.rb RSpec/HookArgument: Description: Prefer explicit :each argument, matching existing module's style EnforcedStyle: each Style/BlockDelimiters: Description: Prefer braces for chaining. Mostly an aesthetical choice. Better to be consistent then. EnforcedStyle: braces_for_chaining Style/ClassAndModuleChildren: Description: Compact style reduces the required amount of indentation. EnforcedStyle: compact Style/EmptyElse: Description: Enforce against empty else clauses, but allow `nil` for clarity. EnforcedStyle: empty Style/FormatString: Description: Following the main puppet project's style, prefer the % format format. EnforcedStyle: percent Style/FormatStringToken: Description: Following the main puppet project's style, prefer the simpler template tokens over annotated ones. EnforcedStyle: template Style/Lambda: Description: Prefer the keyword for easier discoverability. EnforcedStyle: literal Style/RegexpLiteral: Description: Community preference. See https://github.com/voxpupuli/modulesync_config/issues/168 EnforcedStyle: percent_r Style/TernaryParentheses: Description: Checks for use of parentheses around ternary conditions. Enforce parentheses on complex expressions for better readability, but seriously consider breaking it up. EnforcedStyle: require_parentheses_when_complex Style/TrailingCommaInArguments: Description: Prefer always trailing comma on multiline argument lists. This makes diffs, and re-ordering nicer. EnforcedStyleForMultiline: comma Style/TrailingCommaInArrayLiteral: Description: Prefer always trailing comma on multiline literals. This makes diffs, and re-ordering nicer. EnforcedStyleForMultiline: comma Style/SymbolArray: Description: Using percent style obscures symbolic intent of array's contents. EnforcedStyle: brackets RSpec/MessageSpies: EnforcedStyle: receive Style/Documentation: Exclude: - lib/puppet/parser/functions/**/* - spec/**/* Style/WordArray: EnforcedStyle: brackets Performance/AncestorsInclude: Enabled: true Performance/BigDecimalWithNumericArgument: Enabled: true Performance/BlockGivenWithExplicitBlock: Enabled: true -Performance/Caller: - Enabled: true Performance/CaseWhenSplat: Enabled: true -Performance/Casecmp: - Enabled: true -Performance/CollectionLiteralInLoop: - Enabled: true -Performance/CompareWithBlock: - Enabled: true Performance/ConstantRegexp: Enabled: true -Performance/Count: - Enabled: true -Performance/Detect: - Enabled: true -Performance/DoubleStartEndWith: - Enabled: true -Performance/EndWith: - Enabled: true -Performance/FixedSize: - Enabled: true -Performance/FlatMap: - Enabled: true Performance/MethodObjectAsBlock: Enabled: true -Performance/RangeInclude: - Enabled: true -Performance/RedundantBlockCall: - Enabled: true -Performance/RedundantMatch: - Enabled: true -Performance/RedundantMerge: - Enabled: true Performance/RedundantSortBlock: Enabled: true Performance/RedundantStringChars: Enabled: true -Performance/RegexpMatch: - Enabled: true -Performance/ReverseEach: - Enabled: true Performance/ReverseFirst: Enabled: true -Performance/Size: - Enabled: true Performance/SortReverse: Enabled: true Performance/Squeeze: Enabled: true -Performance/StartWith: - Enabled: true Performance/StringInclude: Enabled: true -Performance/StringReplacement: - Enabled: true Performance/Sum: Enabled: true -Performance/TimesMap: - Enabled: true Style/CollectionMethods: Enabled: true Style/MethodCalledOnDoEndBlock: Enabled: true Style/StringMethods: Enabled: true Bundler/InsecureProtocolSource: Enabled: false Gemspec/DuplicatedAssignment: Enabled: false Gemspec/OrderedDependencies: Enabled: false Gemspec/RequiredRubyVersion: Enabled: false Gemspec/RubyVersionGlobalsUsage: Enabled: false Layout/ArgumentAlignment: Enabled: false Layout/BeginEndAlignment: Enabled: false Layout/ClosingHeredocIndentation: Enabled: false Layout/EmptyComment: Enabled: false Layout/EmptyLineAfterGuardClause: Enabled: false Layout/EmptyLinesAroundArguments: Enabled: false Layout/EmptyLinesAroundAttributeAccessor: Enabled: false Layout/EndOfLine: Enabled: false Layout/FirstArgumentIndentation: Enabled: false Layout/HashAlignment: Enabled: false Layout/HeredocIndentation: Enabled: false Layout/LeadingEmptyLines: Enabled: false Layout/SpaceAroundMethodCallOperator: Enabled: false Layout/SpaceInsideArrayLiteralBrackets: Enabled: false Layout/SpaceInsideReferenceBrackets: Enabled: false Lint/BigDecimalNew: Enabled: false Lint/BooleanSymbol: Enabled: false Lint/ConstantDefinitionInBlock: Enabled: false Lint/DeprecatedOpenSSLConstant: Enabled: false Lint/DisjunctiveAssignmentInConstructor: Enabled: false -Lint/DuplicateBranch: - Enabled: false Lint/DuplicateElsifCondition: Enabled: false -Lint/DuplicateRegexpCharacterClassElement: - Enabled: false Lint/DuplicateRequire: Enabled: false Lint/DuplicateRescueException: Enabled: false -Lint/EmptyBlock: - Enabled: false -Lint/EmptyClass: - Enabled: false Lint/EmptyConditionalBody: Enabled: false Lint/EmptyFile: Enabled: false Lint/ErbNewArguments: Enabled: false Lint/FloatComparison: Enabled: false Lint/HashCompareByIdentity: Enabled: false Lint/IdentityComparison: Enabled: false Lint/InterpolationCheck: Enabled: false Lint/MissingCopEnableDirective: Enabled: false Lint/MixedRegexpCaptureTypes: Enabled: false Lint/NestedPercentLiteral: Enabled: false -Lint/NoReturnInBeginEndBlocks: - Enabled: false Lint/NonDeterministicRequireOrder: Enabled: false Lint/OrderedMagicComments: Enabled: false Lint/OutOfRangeRegexpRef: Enabled: false Lint/RaiseException: Enabled: false Lint/RedundantCopEnableDirective: Enabled: false Lint/RedundantRequireStatement: Enabled: false Lint/RedundantSafeNavigation: Enabled: false Lint/RedundantWithIndex: Enabled: false Lint/RedundantWithObject: Enabled: false Lint/RegexpAsCondition: Enabled: false Lint/ReturnInVoidContext: Enabled: false Lint/SafeNavigationConsistency: Enabled: false Lint/SafeNavigationWithEmpty: Enabled: false Lint/SelfAssignment: Enabled: false Lint/SendWithMixinArgument: Enabled: false Lint/ShadowedArgument: Enabled: false Lint/StructNewOverride: Enabled: false -Lint/ToEnumArguments: - Enabled: false Lint/ToJSON: Enabled: false Lint/TopLevelReturnWithArgument: Enabled: false Lint/TrailingCommaInAttributeDeclaration: Enabled: false -Lint/UnexpectedBlockArity: - Enabled: false -Lint/UnmodifiedReduceAccumulator: - Enabled: false Lint/UnreachableLoop: Enabled: false Lint/UriEscapeUnescape: Enabled: false Lint/UriRegexp: Enabled: false Lint/UselessMethodDefinition: Enabled: false Lint/UselessTimes: Enabled: false Metrics/AbcSize: Enabled: false Metrics/BlockLength: Enabled: false +Metrics/BlockNesting: + Enabled: false Metrics/ClassLength: Enabled: false Metrics/CyclomaticComplexity: Enabled: false Metrics/MethodLength: Enabled: false Metrics/ModuleLength: Enabled: false Metrics/ParameterLists: Enabled: false Metrics/PerceivedComplexity: Enabled: false Migration/DepartmentName: Enabled: false +Naming/AccessorMethodName: + Enabled: false Naming/BlockParameterName: Enabled: false Naming/HeredocDelimiterCase: Enabled: false Naming/HeredocDelimiterNaming: Enabled: false Naming/MemoizedInstanceVariableName: Enabled: false Naming/MethodParameterName: Enabled: false Naming/RescuedExceptionsVariableName: Enabled: false +Performance/BindCall: + Enabled: false +Performance/DeletePrefix: + Enabled: false +Performance/DeleteSuffix: + Enabled: false +Performance/InefficientHashSearch: + Enabled: false +Performance/UnfreezeString: + Enabled: false +Performance/UriDefaultParser: + Enabled: false RSpec/Be: Enabled: false RSpec/Capybara/CurrentPathExpectation: Enabled: false RSpec/Capybara/FeatureMethods: Enabled: false RSpec/Capybara/VisibilityMatcher: Enabled: false RSpec/ContextMethod: Enabled: false RSpec/ContextWording: Enabled: false RSpec/DescribeClass: Enabled: false RSpec/EmptyHook: Enabled: false RSpec/EmptyLineAfterExample: Enabled: false RSpec/EmptyLineAfterExampleGroup: Enabled: false RSpec/EmptyLineAfterHook: Enabled: false RSpec/ExampleLength: Enabled: false RSpec/ExampleWithoutDescription: Enabled: false RSpec/ExpectChange: Enabled: false RSpec/ExpectInHook: Enabled: false RSpec/FactoryBot/AttributeDefinedStatically: Enabled: false RSpec/FactoryBot/CreateList: Enabled: false RSpec/FactoryBot/FactoryClassName: Enabled: false RSpec/HooksBeforeExamples: Enabled: false RSpec/ImplicitBlockExpectation: Enabled: false RSpec/ImplicitSubject: Enabled: false RSpec/LeakyConstantDeclaration: Enabled: false RSpec/LetBeforeExamples: Enabled: false RSpec/MissingExampleGroupArgument: Enabled: false RSpec/MultipleExpectations: Enabled: false RSpec/MultipleMemoizedHelpers: Enabled: false RSpec/MultipleSubjects: Enabled: false RSpec/NestedGroups: Enabled: false RSpec/PredicateMatcher: Enabled: false RSpec/ReceiveCounts: Enabled: false RSpec/ReceiveNever: Enabled: false RSpec/RepeatedExampleGroupBody: Enabled: false RSpec/RepeatedExampleGroupDescription: Enabled: false RSpec/RepeatedIncludeExample: Enabled: false RSpec/ReturnFromStub: Enabled: false RSpec/SharedExamples: Enabled: false RSpec/StubbedMock: Enabled: false RSpec/UnspecifiedException: Enabled: false RSpec/VariableDefinition: Enabled: false RSpec/VoidExpect: Enabled: false RSpec/Yield: Enabled: false Security/Open: Enabled: false Style/AccessModifierDeclarations: Enabled: false Style/AccessorGrouping: Enabled: false -Style/ArgumentsForwarding: - Enabled: false Style/AsciiComments: Enabled: false Style/BisectedAttrAccessor: Enabled: false Style/CaseLikeIf: Enabled: false Style/ClassEqualityComparison: Enabled: false -Style/CollectionCompact: - Enabled: false Style/ColonMethodDefinition: Enabled: false Style/CombinableLoops: Enabled: false Style/CommentedKeyword: Enabled: false Style/Dir: Enabled: false -Style/DocumentDynamicEvalDefinition: - Enabled: false Style/DoubleCopDisableDirective: Enabled: false Style/EmptyBlockParameter: Enabled: false Style/EmptyLambdaParameter: Enabled: false Style/Encoding: Enabled: false Style/EvalWithLocation: Enabled: false Style/ExpandPathArguments: Enabled: false Style/ExplicitBlockArgument: Enabled: false Style/ExponentialNotation: Enabled: false Style/FloatDivision: Enabled: false Style/GlobalStdStream: Enabled: false Style/HashAsLastArrayItem: Enabled: false Style/HashLikeCase: Enabled: false Style/HashTransformKeys: Enabled: false Style/HashTransformValues: Enabled: false Style/IfUnlessModifier: Enabled: false Style/KeywordParametersOrder: Enabled: false Style/MinMax: Enabled: false Style/MixinUsage: Enabled: false Style/MultilineWhenThen: Enabled: false -Style/NegatedIfElseCondition: - Enabled: false Style/NegatedUnless: Enabled: false -Style/NilLambda: - Enabled: false Style/NumericPredicate: Enabled: false Style/OptionalBooleanParameter: Enabled: false Style/OrAssignment: Enabled: false Style/RandomWithOffset: Enabled: false -Style/RedundantArgument: - Enabled: false Style/RedundantAssignment: Enabled: false Style/RedundantCondition: Enabled: false Style/RedundantConditional: Enabled: false Style/RedundantFetchBlock: Enabled: false Style/RedundantFileExtensionInRequire: Enabled: false Style/RedundantRegexpCharacterClass: Enabled: false Style/RedundantRegexpEscape: Enabled: false Style/RedundantSelfAssignment: Enabled: false Style/RedundantSort: Enabled: false Style/RescueStandardError: Enabled: false Style/SingleArgumentDig: Enabled: false Style/SlicingWithRange: Enabled: false Style/SoleNestedConditional: Enabled: false Style/StderrPuts: Enabled: false Style/StringConcatenation: Enabled: false Style/Strip: Enabled: false -Style/SwapValues: - Enabled: false Style/SymbolProc: Enabled: false Style/TrailingBodyOnClass: Enabled: false Style/TrailingBodyOnMethodDefinition: Enabled: false Style/TrailingBodyOnModule: Enabled: false Style/TrailingCommaInHashLiteral: Enabled: false Style/TrailingMethodEndStatement: Enabled: false Style/UnpackFirst: Enabled: false +Lint/DuplicateBranch: + Enabled: false +Lint/DuplicateRegexpCharacterClassElement: + Enabled: false +Lint/EmptyBlock: + Enabled: false +Lint/EmptyClass: + Enabled: false +Lint/NoReturnInBeginEndBlocks: + Enabled: false +Lint/ToEnumArguments: + Enabled: false +Lint/UnexpectedBlockArity: + Enabled: false +Lint/UnmodifiedReduceAccumulator: + Enabled: false +Performance/CollectionLiteralInLoop: + Enabled: false +Style/ArgumentsForwarding: + Enabled: false +Style/CollectionCompact: + Enabled: false +Style/DocumentDynamicEvalDefinition: + Enabled: false +Style/NegatedIfElseCondition: + Enabled: false +Style/NilLambda: + Enabled: false +Style/RedundantArgument: + Enabled: false +Style/SwapValues: + Enabled: false diff --git a/lib/puppet/provider/mysql.rb b/lib/puppet/provider/mysql.rb index 0a6d581..40d7b8d 100644 --- a/lib/puppet/provider/mysql.rb +++ b/lib/puppet/provider/mysql.rb @@ -1,177 +1,177 @@ # frozen_string_literal: true # Puppet provider for mysql class Puppet::Provider::Mysql < Puppet::Provider # Without initvars commands won't work. initvars # Make sure we find mysql commands on CentOS and FreeBSD ENV['PATH'] = ENV['PATH'] + ':/usr/libexec:/usr/local/libexec:/usr/local/bin' ENV['LD_LIBRARY_PATH'] = [ ENV['LD_LIBRARY_PATH'], '/usr/lib', '/usr/lib64', '/opt/rh/rh-mysql56/root/usr/lib', '/opt/rh/rh-mysql56/root/usr/lib64', '/opt/rh/rh-mysql57/root/usr/lib', '/opt/rh/rh-mysql57/root/usr/lib64', '/opt/rh/rh-mysql80/root/usr/lib', '/opt/rh/rh-mysql80/root/usr/lib64', '/opt/rh/rh-mariadb100/root/usr/lib', '/opt/rh/rh-mariadb100/root/usr/lib64', '/opt/rh/rh-mariadb101/root/usr/lib', '/opt/rh/rh-mariadb101/root/usr/lib64', '/opt/rh/rh-mariadb102/root/usr/lib', '/opt/rh/rh-mariadb102/root/usr/lib64', '/opt/rh/rh-mariadb103/root/usr/lib', '/opt/rh/rh-mariadb103/root/usr/lib64', '/opt/rh/mysql55/root/usr/lib', '/opt/rh/mysql55/root/usr/lib64', '/opt/rh/mariadb55/root/usr/lib', '/opt/rh/mariadb55/root/usr/lib64', '/usr/mysql/5.5/lib', '/usr/mysql/5.5/lib64', '/usr/mysql/5.6/lib', '/usr/mysql/5.6/lib64', '/usr/mysql/5.7/lib', '/usr/mysql/5.7/lib64', ].join(':') # rubocop:disable Style/HashSyntax commands :mysql_raw => 'mysql' commands :mysqld => 'mysqld' commands :mysqladmin => 'mysqladmin' # rubocop:enable Style/HashSyntax # Optional defaults file def self.defaults_file "--defaults-extra-file=#{Facter.value(:root_home)}/.my.cnf" if File.file?("#{Facter.value(:root_home)}/.my.cnf") end def self.mysqld_type # find the mysql "dialect" like mariadb / mysql etc. mysqld_version_string.scan(%r{mariadb}i) { return 'mariadb' } mysqld_version_string.scan(%r{\s\(percona}i) { return 'percona' } 'mysql' end def mysqld_type self.class.mysqld_type end def self.mysqld_version_string # As the possibility of the mysqld being remote we need to allow the version string to be overridden, # this can be done by facter.value as seen below. In the case that it has not been set and the facter # value is nil we use the mysql -v command to ensure we report the correct version of mysql for later use cases. @mysqld_version_string ||= Facter.value(:mysqld_version) || mysqld('-V') end def mysqld_version_string self.class.mysqld_version_string end def self.mysqld_version # NOTE: be prepared for '5.7.6-rc-log' etc results # versioncmp detects 5.7.6-log to be newer then 5.7.6 # this is why we need the trimming. - mysqld_version_string.scan(%r{\d+\.\d+\.\d+}).first unless mysqld_version_string.nil? + mysqld_version_string&.scan(%r{\d+\.\d+\.\d+})&.first end def mysqld_version self.class.mysqld_version end def self.newer_than(forks_versions) - forks_versions.keys.include?(mysqld_type) && Puppet::Util::Package.versioncmp(mysqld_version, forks_versions[mysqld_type]) >= 0 + forks_versions.key?(mysqld_type) && Puppet::Util::Package.versioncmp(mysqld_version, forks_versions[mysqld_type]) >= 0 end def newer_than(forks_versions) self.class.newer_than(forks_versions) end def self.older_than(forks_versions) - forks_versions.keys.include?(mysqld_type) && Puppet::Util::Package.versioncmp(mysqld_version, forks_versions[mysqld_type]) < 0 + forks_versions.key?(mysqld_type) && Puppet::Util::Package.versioncmp(mysqld_version, forks_versions[mysqld_type]) < 0 end def older_than(forks_versions) self.class.older_than(forks_versions) end def defaults_file self.class.defaults_file end def self.mysql_caller(text_of_sql, type) if type.eql? 'system' if File.file?("#{Facter.value(:root_home)}/.mylogin.cnf") ENV['MYSQL_TEST_LOGIN_FILE'] = "#{Facter.value(:root_home)}/.mylogin.cnf" mysql_raw([system_database, '-e', text_of_sql].flatten.compact).scrub else mysql_raw([defaults_file, system_database, '-e', text_of_sql].flatten.compact).scrub end elsif type.eql? 'regular' if File.file?("#{Facter.value(:root_home)}/.mylogin.cnf") ENV['MYSQL_TEST_LOGIN_FILE'] = "#{Facter.value(:root_home)}/.mylogin.cnf" mysql_raw(['-NBe', text_of_sql].flatten.compact).scrub else mysql_raw([defaults_file, '-NBe', text_of_sql].flatten.compact).scrub end else raise Puppet::Error, _("#mysql_caller: Unrecognised type '%{type}'" % { type: type }) end end def self.users mysql_caller("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').split("\n") end # Optional parameter to run a statement on the MySQL system database. def self.system_database '--database=mysql' end def system_database self.class.system_database end # Take root@localhost and munge it to 'root'@'localhost' # Take root@id123@localhost and munge it to 'root@id123'@'localhost' def self.cmd_user(user) "'#{user.reverse.sub('@', "'@'").reverse}'" end # Take root.* and return ON `root`.* def self.cmd_table(table) table_string = '' # We can't escape *.* so special case this. table_string << if table == '*.*' '*.*' # Special case also for FUNCTIONs and PROCEDUREs elsif table.start_with?('FUNCTION ', 'PROCEDURE ') table.sub(%r{^(FUNCTION|PROCEDURE) (.*)(\..*)}, '\1 `\2`\3') else table.sub(%r{^(.*)(\..*)}, '`\1`\2') end table_string end def self.cmd_privs(privileges) return 'ALL PRIVILEGES' if privileges.include?('ALL') priv_string = '' privileges.each do |priv| priv_string << "#{priv}, " end # Remove trailing , from the last element. priv_string.sub(%r{, $}, '') end # Take in potential options and build up a query string with them. def self.cmd_options(options) option_string = '' options.each do |opt| option_string << ' WITH GRANT OPTION' if opt == 'GRANT' end option_string end end diff --git a/lib/puppet/provider/mysql_database/mysql.rb b/lib/puppet/provider/mysql_database/mysql.rb index d75260e..04d95bf 100644 --- a/lib/puppet/provider/mysql_database/mysql.rb +++ b/lib/puppet/provider/mysql_database/mysql.rb @@ -1,67 +1,67 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) Puppet::Type.type(:mysql_database).provide(:mysql, parent: Puppet::Provider::Mysql) do desc 'Manages MySQL databases.' commands mysql_raw: 'mysql' def self.instances mysql_caller('show databases', 'regular').split("\n").map do |name| attributes = {} mysql_caller(["show variables like '%_database'", name], 'regular').split("\n").each do |line| k, v = line.split(%r{\s}) attributes[k] = v end new(name: name, ensure: :present, charset: attributes['character_set_database'], collate: attributes['collation_database']) end end # We iterate over each mysql_database entry in the catalog and compare it against # the contents of the property_hash generated by self.instances def self.prefetch(resources) databases = instances - resources.keys.each do |database| + resources.each_key do |database| provider = databases.find { |db| db.name == database } resources[database].provider = provider if provider end end def create self.class.mysql_caller("create database if not exists `#{@resource[:name]}` character set `#{@resource[:charset]}` collate `#{@resource[:collate]}`", 'regular') @property_hash[:ensure] = :present @property_hash[:charset] = @resource[:charset] @property_hash[:collate] = @resource[:collate] exists? ? (return true) : (return false) end def destroy self.class.mysql_caller("drop database if exists `#{@resource[:name]}`", 'regular') @property_hash.clear exists? ? (return false) : (return true) end def exists? @property_hash[:ensure] == :present || false end mk_resource_methods def charset=(value) self.class.mysql_caller("alter database `#{resource[:name]}` CHARACTER SET #{value}", 'regular') @property_hash[:charset] = value (charset == value) ? (return true) : (return false) end def collate=(value) self.class.mysql_caller("alter database `#{resource[:name]}` COLLATE #{value}", 'regular') @property_hash[:collate] = value (collate == value) ? (return true) : (return false) end end diff --git a/lib/puppet/provider/mysql_grant/mysql.rb b/lib/puppet/provider/mysql_grant/mysql.rb index 12999fe..5d4947a 100644 --- a/lib/puppet/provider/mysql_grant/mysql.rb +++ b/lib/puppet/provider/mysql_grant/mysql.rb @@ -1,178 +1,178 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) Puppet::Type.type(:mysql_grant).provide(:mysql, parent: Puppet::Provider::Mysql) do desc 'Set grants for users in MySQL.' commands mysql_raw: 'mysql' def self.instances instances = [] users.map do |user| user_string = cmd_user(user) query = "SHOW GRANTS FOR #{user_string};" begin grants = mysql_caller(query, 'regular') rescue Puppet::ExecutionFailure => e # Silently ignore users with no grants. Can happen e.g. if user is # defined with fqdn and server is run with skip-name-resolve. Example: # Default root user created by mysql_install_db on a host with fqdn # of myhost.mydomain.my: root@myhost.mydomain.my, when MySQL is started # with --skip-name-resolve. next if %r{There is no such grant defined for user}.match?(e.inspect) raise Puppet::Error, _('#mysql had an error -> %{inspect}') % { inspect: e.inspect } end # Once we have the list of grants generate entries for each. grants.each_line do |grant| # Match the munges we do in the type. munged_grant = grant.delete("'").delete('`').delete('"') # Matching: GRANT (SELECT, UPDATE) PRIVILEGES ON (*.*) TO ('root')@('127.0.0.1') (WITH GRANT OPTION) next unless match = munged_grant.match(%r{^GRANT\s(.+)\sON\s(.+)\sTO\s(.*)@(.*?)(\s.*)?$}) # rubocop:disable Lint/AssignmentInCondition privileges, table, user, host, rest = match.captures table.gsub!('\\\\', '\\') # split on ',' if it is not a non-'('-containing string followed by a # closing parenthesis ')'-char - e.g. only split comma separated elements not in # parentheses stripped_privileges = privileges.strip.split(%r{\s*,\s*(?![^(]*\))}).map do |priv| # split and sort the column_privileges in the parentheses and rejoin if priv.include?('(') type, col = priv.strip.split(%r{\s+|\b}, 2) type.upcase + ' (' + col.slice(1...-1).strip.split(%r{\s*,\s*}).sort.join(', ') + ')' else # Once we split privileges up on the , we need to make sure we # shortern ALL PRIVILEGES to just all. (priv == 'ALL PRIVILEGES') ? 'ALL' : priv.strip end end # Same here, but to remove OPTION leaving just GRANT. options = if %r{WITH\sGRANT\sOPTION}.match?(rest) ['GRANT'] else ['NONE'] end # fix double backslash that MySQL prints, so resources match table.gsub!('\\\\', '\\') # We need to return an array of instances so capture these instances << new( name: "#{user}@#{host}/#{table}", ensure: :present, privileges: stripped_privileges.sort, table: table, user: "#{user}@#{host}", options: options, ) end end instances end def self.prefetch(resources) users = instances - resources.keys.each do |name| + resources.each_key do |name| if provider = users.find { |user| user.name == name } # rubocop:disable Lint/AssignmentInCondition resources[name].provider = provider end end end def grant(user, table, privileges, options) user_string = self.class.cmd_user(user) priv_string = self.class.cmd_privs(privileges) table_string = privileges.include?('PROXY') ? self.class.cmd_user(table) : self.class.cmd_table(table) query = "GRANT #{priv_string}" query << " ON #{table_string}" query << " TO #{user_string}" query << self.class.cmd_options(options) unless options.nil? self.class.mysql_caller(query, 'system') end def create grant(@resource[:user], @resource[:table], @resource[:privileges], @resource[:options]) @property_hash[:ensure] = :present @property_hash[:table] = @resource[:table] @property_hash[:user] = @resource[:user] @property_hash[:options] = @resource[:options] if @resource[:options] @property_hash[:privileges] = @resource[:privileges] exists? ? (return true) : (return false) end def revoke(user, table, revoke_privileges = ['ALL']) user_string = self.class.cmd_user(user) table_string = revoke_privileges.include?('PROXY') ? self.class.cmd_user(table) : self.class.cmd_table(table) priv_string = self.class.cmd_privs(revoke_privileges) # revoke grant option needs to be a extra query, because # "REVOKE ALL PRIVILEGES, GRANT OPTION [..]" is only valid mysql syntax # if no ON clause is used. # It hast to be executed before "REVOKE ALL [..]" since a GRANT has to # exist to be executed successfully if revoke_privileges.include?('ALL') && !revoke_privileges.include?('PROXY') query = "REVOKE GRANT OPTION ON #{table_string} FROM #{user_string}" self.class.mysql_caller(query, 'system') end query = "REVOKE #{priv_string} ON #{table_string} FROM #{user_string}" self.class.mysql_caller(query, 'system') end def destroy # if the user was dropped, it'll have been removed from the user hash # as the grants are already removed by the DROP statement if self.class.users.include? @property_hash[:user] if @property_hash[:privileges].include?('PROXY') revoke(@property_hash[:user], @property_hash[:table], @property_hash[:privileges]) else revoke(@property_hash[:user], @property_hash[:table]) end end @property_hash.clear exists? ? (return false) : (return true) end def exists? @property_hash[:ensure] == :present || false end def flush @property_hash.clear self.class.mysql_caller('FLUSH PRIVILEGES', 'regular') end mk_resource_methods def diff_privileges(privileges_old, privileges_new) diff = { revoke: [], grant: [] } if privileges_old.include? 'ALL' diff[:revoke] = privileges_old diff[:grant] = privileges_new elsif privileges_new.include? 'ALL' diff[:grant] = privileges_new else diff[:revoke] = privileges_old - privileges_new diff[:grant] = privileges_new - privileges_old end diff end def privileges=(privileges) diff = diff_privileges(@property_hash[:privileges], privileges) unless diff[:revoke].empty? revoke(@property_hash[:user], @property_hash[:table], diff[:revoke]) end unless diff[:grant].empty? grant(@property_hash[:user], @property_hash[:table], diff[:grant], @property_hash[:options]) end @property_hash[:privileges] = privileges self.privileges end def options=(options) revoke(@property_hash[:user], @property_hash[:table]) grant(@property_hash[:user], @property_hash[:table], @property_hash[:privileges], options) @property_hash[:options] = options self.options end end diff --git a/lib/puppet/provider/mysql_login_path/inifile.rb b/lib/puppet/provider/mysql_login_path/inifile.rb index 0ed5480..005d6d5 100644 --- a/lib/puppet/provider/mysql_login_path/inifile.rb +++ b/lib/puppet/provider/mysql_login_path/inifile.rb @@ -1,633 +1,635 @@ # encoding: UTF-8 # frozen_string_literal: true # See: https://github.com/puppetlabs/puppet/blob/main/lib/puppet/util/inifile.rb # This class represents the INI file and can be used to parse, modify, # and write INI files. class Puppet::Provider::MysqlLoginPath::IniFile < Puppet::Provider include Enumerable class Error < StandardError; end # VERSION = '3.0.0' # Public: Open an INI file and load the contents. # # filename - The name of the file as a String # opts - The Hash of options (default: {}) # :comment - String containing the comment character(s) # :parameter - String used to separate parameter and value # :encoding - Encoding String for reading / writing # :default - The String name of the default global section # # Examples # # IniFile.load('file.ini') # #=> IniFile instance # # IniFile.load('does/not/exist.ini') # #=> nil # # Returns an IniFile instance or nil if the file could not be opened. def self.load(filename, opts = {}) return unless File.file? filename new(opts.merge(filename: filename)) end # Get and set the filename attr_accessor :filename # Get and set the encoding attr_accessor :encoding # Public: Create a new INI file from the given set of options. If :content # is provided then it will be used to populate the INI file. If a :filename # is provided then the contents of the file will be parsed and stored in the # INI file. If neither the :content or :filename is provided then an empty # INI file is created. # # opts - The Hash of options (default: {}) # :content - The String/Hash containing the INI contents # :comment - String containing the comment character(s) # :parameter - String used to separate parameter and value # :encoding - Encoding String for reading / writing # :default - The String name of the default global section # :filename - The filename as a String # # Examples # # IniFile.new # #=> an empty IniFile instance # # IniFile.new( :content => "[global]\nfoo=bar" ) # #=> an IniFile instance # # IniFile.new( :filename => 'file.ini', :encoding => 'UTF-8' ) # #=> an IniFile instance # # IniFile.new( :content => "[global]\nfoo=bar", :comment => '#' ) # #=> an IniFile instance # def initialize(opts = {}) + super + @comment = opts.fetch(:comment, ';#') @param = opts.fetch(:parameter, '=') @encoding = opts.fetch(:encoding, nil) @default = opts.fetch(:default, 'global') @filename = opts.fetch(:filename, nil) content = opts.fetch(:content, nil) @ini = Hash.new { |h, k| h[k] = {} } if content.is_a?(Hash) then merge!(content) elsif content then parse(content) elsif @filename then read end end # Public: Write the contents of this IniFile to the file system. If left # unspecified, the currently configured filename and encoding will be used. # Otherwise the filename and encoding can be specified in the options hash. # # opts - The default options Hash # :filename - The filename as a String # :encoding - The encoding as a String # # Returns this IniFile instance. def write(opts = {}) filename = opts.fetch(:filename, @filename) encoding = opts.fetch(:encoding, @encoding) mode = encoding ? "w:#{encoding}" : 'w' File.open(filename, mode) do |f| @ini.each do |section, hash| f.puts "[#{section}]" hash.each { |param, val| f.puts "#{param} #{@param} #{escape_value val}" } f.puts end end self end alias save write # Public: Read the contents of the INI file from the file system and replace # and set the state of this IniFile instance. If left unspecified the # currently configured filename and encoding will be used when reading from # the file system. Otherwise the filename and encoding can be specified in # the options hash. # # opts - The default options Hash # :filename - The filename as a String # :encoding - The encoding as a String # # Returns this IniFile instance if the read was successful; nil is returned # if the file could not be read. def read(opts = {}) filename = opts.fetch(:filename, @filename) encoding = opts.fetch(:encoding, @encoding) return unless File.file? filename mode = encoding ? "r:#{encoding}" : 'r' File.open(filename, mode) { |fd| parse fd } self end alias restore read # Returns this IniFile converted to a String. def to_s s = [] @ini.each do |section, hash| s << "[#{section}]" hash.each { |param, val| s << "#{param} #{@param} #{escape_value val}" } s << '' end s.join("\n") end # Returns this IniFile converted to a Hash. def to_h @ini.dup end # Public: Creates a copy of this inifile with the entries from the # other_inifile merged into the copy. # # other - The other IniFile. # # Returns a new IniFile. def merge(other) dup.merge!(other) end # Public: Merges other_inifile into this inifile, overwriting existing # entries. Useful for having a system inifile with user overridable settings # elsewhere. # # other - The other IniFile. # # Returns this IniFile. def merge!(other) return self if other.nil? my_keys = @ini.keys other_keys = case other when IniFile other.instance_variable_get(:@ini).keys when Hash other.keys else raise Error, "cannot merge contents from '#{other.class.name}'" end (my_keys & other_keys).each do |key| case other[key] when Hash @ini[key].merge!(other[key]) when nil nil else raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}" end end (other_keys - my_keys).each do |key| @ini[key] = case other[key] when Hash other[key].dup when nil {} else raise Error, "cannot merge section #{key.inspect} - unsupported type: #{other[key].class.name}" end end self end # Public: Yield each INI file section, parameter, and value in turn to the # given block. # # block - The block that will be iterated by the each method. The block will # be passed the current section and the parameter/value pair. # # Examples # # inifile.each do |section, parameter, value| # puts "#{parameter} = #{value} [in section - #{section}]" # end # # Returns this IniFile. def each return unless block_given? @ini.each do |section, hash| hash.each do |param, val| yield section, param, val end end self end # Public: Yield each section in turn to the given block. # # block - The block that will be iterated by the each method. The block will # be passed the current section as a Hash. # # Examples # # inifile.each_section do |section| # puts section.inspect # end # # Returns this IniFile. def each_section return unless block_given? @ini.each_key { |section| yield section } self end # Public: Remove a section identified by name from the IniFile. # # section - The section name as a String. # # Returns the deleted section Hash. def delete_section(section) @ini.delete section.to_s end # Public: Get the section Hash by name. If the section does not exist, then # it will be created. # # section - The section name as a String. # # Examples # # inifile['global'] # #=> global section Hash # # Returns the Hash of parameter/value pairs for this section. def [](section) return nil if section.nil? @ini[section.to_s] end # Public: Set the section to a hash of parameter/value pairs. # # section - The section name as a String. # value - The Hash of parameter/value pairs. # # Examples # # inifile['tenderloin'] = { 'gritty' => 'yes' } # #=> { 'gritty' => 'yes' } # # Returns the value Hash. def []=(section, value) @ini[section.to_s] = value end # Public: Create a Hash containing only those INI file sections whose names # match the given regular expression. # # regex - The Regexp used to match section names. # # Examples # # inifile.match(/^tree_/) # #=> Hash of matching sections # # Return a Hash containing only those sections that match the given regular # expression. def match(regex) @ini.dup.delete_if { |section, _| section !~ regex } end # Public: Check to see if the IniFile contains the section. # # section - The section name as a String. # # Returns true if the section exists in the IniFile. def section?(section) @ini.key? section.to_s end # Returns an Array of section names contained in this IniFile. def sections @ini.keys end # Public: Freeze the state of this IniFile object. Any attempts to change # the object will raise an error. # # Returns this IniFile. def freeze super @ini.each_value { |h| h.freeze } @ini.freeze self end # Public: Mark this IniFile as tainted -- this will traverse each section # marking each as tainted. # # Returns this IniFile. def taint super @ini.each_value { |h| h.taint } @ini.taint self end # Public: Produces a duplicate of this IniFile. The duplicate is independent # of the original -- i.e. the duplicate can be modified without changing the # original. The tainted state of the original is copied to the duplicate. # # Returns a new IniFile. def dup other = super other.instance_variable_set(:@ini, Hash.new { |h, k| h[k] = {} }) @ini.each_pair { |s, h| other[s].merge! h } other.taint if tainted? other end # Public: Produces a duplicate of this IniFile. The duplicate is independent # of the original -- i.e. the duplicate can be modified without changing the # original. The tainted state and the frozen state of the original is copied # to the duplicate. # # Returns a new IniFile. def clone other = dup other.freeze if frozen? other end # Public: Compare this IniFile to some other IniFile. For two INI files to # be equivalent, they must have the same sections with the same parameter / # value pairs in each section. # # other - The other IniFile. # # Returns true if the INI files are equivalent and false if they differ. def eql?(other) return true if equal? other return false unless other.instance_of? self.class @ini == other.instance_variable_get(:@ini) end alias == eql? # Escape special characters. # # value - The String value to escape. # # Returns the escaped value. def escape_value(value) value = value.to_s.dup value.gsub!(%r{\\([0nrt])}, '\\\\\1') value.gsub!(%r{\n}, '\n') value.gsub!(%r{\r}, '\r') value.gsub!(%r{\t}, '\t') value.gsub!(%r{\0}, '\0') value end # Parse the given content and store the information in this IniFile # instance. All data will be cleared out and replaced with the information # read from the content. # # content - A String or a file descriptor (must respond to `each_line`) # # Returns this IniFile. def parse(content) parser = Parser.new(@ini, @param, @comment, @default) parser.parse(content) self end # The IniFile::Parser has the responsibility of reading the contents of an # .ini file and storing that information into a ruby Hash. The object being # parsed must respond to `each_line` - this includes Strings and any IO # object. class Parser attr_writer :section attr_accessor :property attr_accessor :value # Create a new IniFile::Parser that can be used to parse the contents of # an .ini file. # # hash - The Hash where parsed information will be stored # param - String used to separate parameter and value # comment - String containing the comment character(s) # default - The String name of the default global section # def initialize(hash, param, comment, default) @hash = hash @default = default comment = comment.to_s.empty? ? '\\z' : "\\s*(?:[#{comment}].*)?\\z" @section_regexp = %r{\A\s*\[([^\]]+)\]#{comment}} @ignore_regexp = %r{\A#{comment}} @property_regexp = %r{\A(.*?)(? true # "false" --> false # "" --> nil # "42" --> 42 # "3.14" --> 3.14 # "foo" --> "foo" # # Returns the typecast value. def typecast(value) case value when %r{\Atrue\z}i then true when %r{\Afalse\z}i then false when %r{\A\s*\z}i then nil else begin begin Integer(value) rescue Float(value) end rescue unescape_value(value) end end end # Unescape special characters found in the value string. This will convert # escaped null, tab, carriage return, newline, and backslash into their # literal equivalents. # # value - The String value to unescape. # # Returns the unescaped value. def unescape_value(value) value = value.to_s value.gsub!(%r{\\[0nrt\\]}) do |char| case char when '\0' then "\0" when '\n' then "\n" when '\r' then "\r" when '\t' then "\t" when '\\\\' then '\\' end end value end end end # IniFile diff --git a/lib/puppet/provider/mysql_plugin/mysql.rb b/lib/puppet/provider/mysql_plugin/mysql.rb index f400809..0a331f4 100644 --- a/lib/puppet/provider/mysql_plugin/mysql.rb +++ b/lib/puppet/provider/mysql_plugin/mysql.rb @@ -1,53 +1,53 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) Puppet::Type.type(:mysql_plugin).provide(:mysql, parent: Puppet::Provider::Mysql) do desc 'Manages MySQL plugins.' commands mysql_raw: 'mysql' def self.instances mysql_caller('show plugins', 'regular').split("\n").map do |line| name, _status, _type, library, _license = line.split(%r{\t}) new(name: name, ensure: :present, soname: library) end end # We iterate over each mysql_plugin entry in the catalog and compare it against # the contents of the property_hash generated by self.instances def self.prefetch(resources) plugins = instances - resources.keys.each do |plugin| + resources.each_key do |plugin| if provider = plugins.find { |pl| pl.name == plugin } # rubocop:disable Lint/AssignmentInCondition resources[plugin].provider = provider end end end def create # Use plugin_name.so as soname if it's not specified. This won't work on windows as # there it should be plugin_name.dll @resource[:soname].nil? ? (soname = @resource[:name] + '.so') : (soname = @resource[:soname]) self.class.mysql_caller("install plugin #{@resource[:name]} soname '#{soname}'", 'regular') @property_hash[:ensure] = :present @property_hash[:soname] = @resource[:soname] exists? ? (return true) : (return false) end def destroy self.class.mysql_caller("uninstall plugin #{@resource[:name]}", 'regular') @property_hash.clear exists? ? (return false) : (return true) end def exists? @property_hash[:ensure] == :present || false end mk_resource_methods end diff --git a/lib/puppet/provider/mysql_user/mysql.rb b/lib/puppet/provider/mysql_user/mysql.rb index 5df9eae..91c1705 100644 --- a/lib/puppet/provider/mysql_user/mysql.rb +++ b/lib/puppet/provider/mysql_user/mysql.rb @@ -1,257 +1,257 @@ # frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'mysql')) Puppet::Type.type(:mysql_user).provide(:mysql, parent: Puppet::Provider::Mysql) do desc 'manage users for a mysql database.' commands mysql_raw: 'mysql' # Build a property_hash containing all the discovered information about MySQL # users. def self.instances users = mysql_caller("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').split("\n") # To reduce the number of calls to MySQL we collect all the properties in # one big swoop. users.map do |name| if mysqld_version.nil? ## Default ... # rubocop:disable Layout/LineLength query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, AUTHENTICATION_STRING, PLUGIN FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" elsif newer_than('mariadb' => '10.1.21') query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD, PLUGIN, AUTHENTICATION_STRING FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" else query = "SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{name}'" end @max_user_connections, @max_connections_per_hour, @max_queries_per_hour, @max_updates_per_hour, ssl_type, ssl_cipher, x509_issuer, x509_subject, @password, @plugin, @authentication_string = mysql_caller(query, 'regular').chomp.split(%r{\t}) @tls_options = parse_tls_options(ssl_type, ssl_cipher, x509_issuer, x509_subject) if newer_than('mariadb' => '10.1.21') && @plugin == 'ed25519' # Some auth plugins (e.g. ed25519) use authentication_string # to store password hash or auth information @password = @authentication_string elsif (newer_than('mariadb' => '10.2.16') && older_than('mariadb' => '10.2.19')) || (newer_than('mariadb' => '10.3.8') && older_than('mariadb' => '10.3.11')) # Old mariadb 10.2 or 10.3 store password hash in authentication_string # https://jira.mariadb.org/browse/MDEV-16238 https://jira.mariadb.org/browse/MDEV-16774 @password = @authentication_string end # rubocop:enable Layout/LineLength new(name: name, ensure: :present, password_hash: @password, plugin: @plugin, max_user_connections: @max_user_connections, max_connections_per_hour: @max_connections_per_hour, max_queries_per_hour: @max_queries_per_hour, max_updates_per_hour: @max_updates_per_hour, tls_options: @tls_options) end end # We iterate over each mysql_user entry in the catalog and compare it against # the contents of the property_hash generated by self.instances def self.prefetch(resources) users = instances # rubocop:disable Lint/AssignmentInCondition - resources.keys.each do |name| + resources.each_key do |name| if provider = users.find { |user| user.name == name } resources[name].provider = provider end end # rubocop:enable Lint/AssignmentInCondition end def create # (MODULES-3539) Allow @ in username merged_name = @resource[:name].reverse.sub('@', "'@'").reverse password_hash = @resource.value(:password_hash) plugin = @resource.value(:plugin) max_user_connections = @resource.value(:max_user_connections) || 0 max_connections_per_hour = @resource.value(:max_connections_per_hour) || 0 max_queries_per_hour = @resource.value(:max_queries_per_hour) || 0 max_updates_per_hour = @resource.value(:max_updates_per_hour) || 0 tls_options = @resource.value(:tls_options) || ['NONE'] # Use CREATE USER to be compatible with NO_AUTO_CREATE_USER sql_mode # This is also required if you want to specify a authentication plugin if !plugin.nil? if !password_hash.nil? self.class.mysql_caller("CREATE USER '#{merged_name}' IDENTIFIED WITH '#{plugin}' AS '#{password_hash}'", 'system') else self.class.mysql_caller("CREATE USER '#{merged_name}' IDENTIFIED WITH '#{plugin}'", 'system') end @property_hash[:ensure] = :present @property_hash[:plugin] = plugin elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.1.3') self.class.mysql_caller("CREATE USER IF NOT EXISTS '#{merged_name}' IDENTIFIED WITH 'mysql_native_password' AS '#{password_hash}'", 'system') @property_hash[:ensure] = :present @property_hash[:password_hash] = password_hash else self.class.mysql_caller("CREATE USER '#{merged_name}' IDENTIFIED BY PASSWORD '#{password_hash}'", 'system') @property_hash[:ensure] = :present @property_hash[:password_hash] = password_hash end # rubocop:disable Layout/LineLength if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6') self.class.mysql_caller("ALTER USER IF EXISTS '#{merged_name}' WITH MAX_USER_CONNECTIONS #{max_user_connections} MAX_CONNECTIONS_PER_HOUR #{max_connections_per_hour} MAX_QUERIES_PER_HOUR #{max_queries_per_hour} MAX_UPDATES_PER_HOUR #{max_updates_per_hour}", 'system') else self.class.mysql_caller("GRANT USAGE ON *.* TO '#{merged_name}' WITH MAX_USER_CONNECTIONS #{max_user_connections} MAX_CONNECTIONS_PER_HOUR #{max_connections_per_hour} MAX_QUERIES_PER_HOUR #{max_queries_per_hour} MAX_UPDATES_PER_HOUR #{max_updates_per_hour}", 'system') end # rubocop:enable Layout/LineLength @property_hash[:max_user_connections] = max_user_connections @property_hash[:max_connections_per_hour] = max_connections_per_hour @property_hash[:max_queries_per_hour] = max_queries_per_hour @property_hash[:max_updates_per_hour] = max_updates_per_hour merged_tls_options = tls_options.join(' AND ') if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') self.class.mysql_caller("ALTER USER '#{merged_name}' REQUIRE #{merged_tls_options}", 'system') else self.class.mysql_caller("GRANT USAGE ON *.* TO '#{merged_name}' REQUIRE #{merged_tls_options}", 'system') end @property_hash[:tls_options] = tls_options exists? ? (return true) : (return false) end def destroy # (MODULES-3539) Allow @ in username merged_name = @resource[:name].reverse.sub('@', "'@'").reverse if_exists = if newer_than('mysql' => '5.7', 'percona' => '5.7', 'mariadb' => '10.1.3') 'IF EXISTS ' else '' end self.class.mysql_caller("DROP USER #{if_exists}'#{merged_name}'", 'system') @property_hash.clear exists? ? (return false) : (return true) end def exists? @property_hash[:ensure] == :present || false end ## ## MySQL user properties ## # Generates method for all properties of the property_hash mk_resource_methods def password_hash=(string) merged_name = self.class.cmd_user(@resource[:name]) plugin = @resource.value(:plugin) # We have a fact for the mysql version ... if mysqld_version.nil? # default ... if mysqld_version does not work self.class.mysql_caller("SET PASSWORD FOR #{merged_name} = '#{string}'", 'system') elsif newer_than('mariadb' => '10.1.21') && plugin == 'ed25519' raise ArgumentError, _('ed25519 hash should be 43 bytes long.') unless string.length == 43 # ALTER USER statement is only available upstream starting 10.2 # https://mariadb.com/kb/en/mariadb-1020-release-notes/ if newer_than('mariadb' => '10.2.0') sql = "ALTER USER #{merged_name} IDENTIFIED WITH ed25519 AS '#{string}'" else concat_name = @resource[:name] sql = "UPDATE mysql.user SET password = '', plugin = 'ed25519'" sql << ", authentication_string = '#{string}'" sql << " where CONCAT(user, '@', host) = '#{concat_name}'; FLUSH PRIVILEGES" end self.class.mysql_caller(sql, 'system') elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') raise ArgumentError, _('Only mysql_native_password (*ABCD...XXX) hashes are supported.') unless %r{^\*|^$}.match?(string) self.class.mysql_caller("ALTER USER #{merged_name} IDENTIFIED WITH mysql_native_password AS '#{string}'", 'system') else self.class.mysql_caller("SET PASSWORD FOR #{merged_name} = '#{string}'", 'system') end (password_hash == string) ? (return true) : (return false) end def max_user_connections=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_USER_CONNECTIONS #{int}", 'system').chomp (max_user_connections == int) ? (return true) : (return false) end def max_connections_per_hour=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_CONNECTIONS_PER_HOUR #{int}", 'system').chomp (max_connections_per_hour == int) ? (return true) : (return false) end def max_queries_per_hour=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_QUERIES_PER_HOUR #{int}", 'system').chomp (max_queries_per_hour == int) ? (return true) : (return false) end def max_updates_per_hour=(int) merged_name = self.class.cmd_user(@resource[:name]) self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} WITH MAX_UPDATES_PER_HOUR #{int}", 'system').chomp (max_updates_per_hour == int) ? (return true) : (return false) end def plugin=(string) merged_name = self.class.cmd_user(@resource[:name]) if newer_than('mariadb' => '10.1.21') && string == 'ed25519' if newer_than('mariadb' => '10.2.0') sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}' AS '#{@resource[:password_hash]}'" else concat_name = @resource[:name] sql = "UPDATE mysql.user SET password = '', plugin = '#{string}'" sql << ", authentication_string = '#{@resource[:password_hash]}'" sql << " where CONCAT(user, '@', host) = '#{concat_name}'; FLUSH PRIVILEGES" end elsif newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') sql = "ALTER USER #{merged_name} IDENTIFIED WITH '#{string}'" sql << " AS '#{@resource[:password_hash]}'" if string == 'mysql_native_password' else # See https://bugs.mysql.com/bug.php?id=67449 sql = "UPDATE mysql.user SET plugin = '#{string}'" sql << ((string == 'mysql_native_password') ? ", password = '#{@resource[:password_hash]}'" : ", password = ''") sql << " WHERE CONCAT(user, '@', host) = '#{@resource[:name]}'" end self.class.mysql_caller(sql, 'system') (plugin == string) ? (return true) : (return false) end def tls_options=(array) merged_name = self.class.cmd_user(@resource[:name]) merged_tls_options = array.join(' AND ') if newer_than('mysql' => '5.7.6', 'percona' => '5.7.6', 'mariadb' => '10.2.0') self.class.mysql_caller("ALTER USER #{merged_name} REQUIRE #{merged_tls_options}", 'system') else self.class.mysql_caller("GRANT USAGE ON *.* TO #{merged_name} REQUIRE #{merged_tls_options}", 'system') end (tls_options == array) ? (return true) : (return false) end def self.parse_tls_options(ssl_type, ssl_cipher, x509_issuer, x509_subject) if ssl_type == 'ANY' ['SSL'] elsif ssl_type == 'X509' ['X509'] elsif ssl_type == 'SPECIFIED' options = [] options << "CIPHER '#{ssl_cipher}'" if !ssl_cipher.nil? && !ssl_cipher.empty? options << "ISSUER '#{x509_issuer}'" if !x509_issuer.nil? && !x509_issuer.empty? options << "SUBJECT '#{x509_subject}'" if !x509_subject.nil? && !x509_subject.empty? options else ['NONE'] end end end diff --git a/lib/puppet/type/mysql_user.rb b/lib/puppet/type/mysql_user.rb index e59b0eb..dafb9b2 100644 --- a/lib/puppet/type/mysql_user.rb +++ b/lib/puppet/type/mysql_user.rb @@ -1,120 +1,120 @@ # frozen_string_literal: true # This has to be a separate type to enable collecting Puppet::Type.newtype(:mysql_user) do @doc = <<-PUPPET @summary Manage a MySQL user. This includes management of users password as well as privileges. PUPPET ensurable autorequire(:file) { '/root/.my.cnf' } autorequire(:class) { 'mysql::server' } newparam(:name, namevar: true) do desc "The name of the user. This uses the 'username@hostname' or username@hostname." validate do |value| # http://dev.mysql.com/doc/refman/5.5/en/identifiers.html # If at least one special char is used, string must be quoted # http://stackoverflow.com/questions/8055727/negating-a-backreference-in-regular-expressions/8057827#8057827 mysql_version = Facter.value(:mysql_version) # rubocop:disable Lint/AssignmentInCondition # rubocop:disable Lint/UselessAssignment if matches = %r{^(['`"])((?:(?!\1).)*)\1@([\w%\.:\-/]+)$}.match(value) user_part = matches[2] host_part = matches[3] elsif matches = %r{^([0-9a-zA-Z$_]*)@([\w%\.:\-/]+)$}.match(value) user_part = matches[1] host_part = matches[2] elsif matches = %r{^((?!['`"]).*[^0-9a-zA-Z$_].*)@(.+)$}.match(value) user_part = matches[1] host_part = matches[2] else raise ArgumentError, _('Invalid database user %{user}.') % { user: value } end # rubocop:enable Lint/AssignmentInCondition # rubocop:enable Lint/UselessAssignment unless mysql_version.nil? raise(ArgumentError, _('MySQL usernames are limited to a maximum of 16 characters.')) if Puppet::Util::Package.versioncmp(mysql_version, '5.7.8') < 0 && user_part.size > 16 raise(ArgumentError, _('MySQL usernames are limited to a maximum of 32 characters.')) if Puppet::Util::Package.versioncmp(mysql_version, '10.0.0') < 0 && user_part.size > 32 raise(ArgumentError, _('MySQL usernames are limited to a maximum of 80 characters.')) if Puppet::Util::Package.versioncmp(mysql_version, '10.0.0') > 0 && user_part.size > 80 end end munge do |value| matches = %r{^((['`"]?).*\2)@(.+)$}.match(value) "#{matches[1]}@#{matches[3].downcase}" end end newproperty(:password_hash) do desc 'The password hash of the user. Use mysql::password() for creating such a hash.' newvalue(%r{\w*}) def change_to_s(currentvalue, _newvalue) (currentvalue == :absent) ? 'created password' : 'changed password' end - # rubocop:disable Style/PredicateName + # rubocop:disable Naming/PredicateName def is_to_s(_currentvalue) '[old password hash redacted]' end - # rubocop:enable Style/PredicateName + # rubocop:enable Naming/PredicateName def should_to_s(_newvalue) '[new password hash redacted]' end end newproperty(:plugin) do desc 'The authentication plugin of the user.' newvalue(%r{\w+}) end newproperty(:max_user_connections) do desc 'Max concurrent connections for the user. 0 means no (or global) limit.' newvalue(%r{\d+}) end newproperty(:max_connections_per_hour) do desc 'Max connections per hour for the user. 0 means no (or global) limit.' newvalue(%r{\d+}) end newproperty(:max_queries_per_hour) do desc 'Max queries per hour for the user. 0 means no (or global) limit.' newvalue(%r{\d+}) end newproperty(:max_updates_per_hour) do desc 'Max updates per hour for the user. 0 means no (or global) limit.' newvalue(%r{\d+}) end newproperty(:tls_options, array_matching: :all) do desc 'Options to that set the TLS-related REQUIRE attributes for the user.' validate do |value| value = [value] unless value.is_a?(Array) if value.include?('NONE') || value.include?('SSL') || value.include?('X509') if value.length > 1 raise(ArgumentError, _('`tls_options` `property`: The values NONE, SSL and X509 cannot be used with other options, you may only pick one of them.')) end else value.each do |opt| o = opt.match(%r{^(CIPHER|ISSUER|SUBJECT)}i) raise(ArgumentError, _('Invalid tls option %{option}.') % { option: o }) unless o end end end def insync?(is) # The current value may be nil and we don't # want to call sort on it so make sure we have arrays if is.is_a?(Array) && @should.is_a?(Array) is.sort == @should.sort else is == @should end end end end diff --git a/spec/acceptance/mysql_backup_spec.rb b/spec/acceptance/mysql_backup_spec.rb index dbfb00a..bdc73d9 100644 --- a/spec/acceptance/mysql_backup_spec.rb +++ b/spec/acceptance/mysql_backup_spec.rb @@ -1,368 +1,364 @@ # frozen_string_literal: true require 'spec_helper_acceptance' describe 'mysql::server::backup class' do context 'should work with no errors' do pp = <<-MANIFEST class { 'mysql::server': root_password => 'password' } mysql::db { [ 'backup1', 'backup2' ]: user => 'backup', password => 'secret', } class { 'mysql::server::backup': backupuser => 'myuser', backuppassword => 'mypassword', backupdir => '/tmp/backups', backupcompress => true, postscript => [ 'rm -rf /var/tmp/mysqlbackups', 'rm -f /var/tmp/mysqlbackups.done', 'cp -r /tmp/backups /var/tmp/mysqlbackups', 'touch /var/tmp/mysqlbackups.done', ], execpath => '/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin', } MANIFEST it 'when configuring mysql backups' do idempotent_apply(pp) end end describe 'mysqlbackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') do before(:all) do pre_run end it 'runs mysqlbackup.sh with no errors' do run_shell('/usr/local/sbin/mysqlbackup.sh') do |r| expect(r.stderr).to eq('') end end it 'dumps all databases to single file' do run_shell('ls -l /tmp/backups/mysql_backup_*-*.sql.bz2 | wc -l') do |r| expect(r.stdout).to match(%r{1}) expect(r.exit_code).to be_zero end end context 'should create one file per database per run' do it 'executes mysqlbackup.sh a second time' do run_shell('sleep 1') run_shell('/usr/local/sbin/mysqlbackup.sh') end it 'creates at least one backup tarball' do run_shell('ls -l /tmp/backups/mysql_backup_*-*.sql.bz2 | wc -l') do |r| expect(r.stdout).to match(%r{2}) expect(r.exit_code).to be_zero end end end end - # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength -end -context 'with one file per database' do - context 'should work with no errors' do - pp = <<-MANIFEST + context 'with one file per database' do + context 'should work with no errors' do + pp = <<-MANIFEST class { 'mysql::server': root_password => 'password' } mysql::db { [ 'backup1', 'backup2' ]: user => 'backup', password => 'secret', } class { 'mysql::server::backup': backupuser => 'myuser', backuppassword => 'mypassword', backupdir => '/tmp/backups', backupcompress => true, file_per_database => true, postscript => [ 'rm -rf /var/tmp/mysqlbackups', 'rm -f /var/tmp/mysqlbackups.done', 'cp -r /tmp/backups /var/tmp/mysqlbackups', 'touch /var/tmp/mysqlbackups.done', ], execpath => '/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin', } MANIFEST - it 'when configuring mysql backups' do - idempotent_apply(pp) + it 'when configuring mysql backups' do + idempotent_apply(pp) + end end - end - describe 'mysqlbackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') do - before(:all) do - pre_run - end + describe 'mysqlbackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') do + before(:all) do + pre_run + end - it 'runs mysqlbackup.sh with no errors without root credentials' do - run_shell('HOME=/tmp/dontreadrootcredentials /usr/local/sbin/mysqlbackup.sh') do |r| - expect(r.stderr).to eq('') + it 'runs mysqlbackup.sh with no errors without root credentials' do + run_shell('HOME=/tmp/dontreadrootcredentials /usr/local/sbin/mysqlbackup.sh') do |r| + expect(r.stderr).to eq('') + end end - end - it 'creates one file per database' do - ['backup1', 'backup2'].each do |database| - run_shell("ls -l /tmp/backups/mysql_backup_#{database}_*-*.sql.bz2 | wc -l") do |r| - expect(r.stdout).to match(%r{1}) - expect(r.exit_code).to be_zero + it 'creates one file per database' do + ['backup1', 'backup2'].each do |database| + run_shell("ls -l /tmp/backups/mysql_backup_#{database}_*-*.sql.bz2 | wc -l") do |r| + expect(r.stdout).to match(%r{1}) + expect(r.exit_code).to be_zero + end end end - end - it 'executes mysqlbackup.sh a second time' do - run_shell('sleep 1') - run_shell('HOME=/tmp/dontreadrootcredentials /usr/local/sbin/mysqlbackup.sh') - end + it 'executes mysqlbackup.sh a second time' do + run_shell('sleep 1') + run_shell('HOME=/tmp/dontreadrootcredentials /usr/local/sbin/mysqlbackup.sh') + end - it 'has one file per database per run' do - ['backup1', 'backup2'].each do |database| - run_shell("ls -l /tmp/backups/mysql_backup_#{database}_*-*.sql.bz2 | wc -l") do |r| - expect(r.stdout).to match(%r{2}) - expect(r.exit_code).to be_zero + it 'has one file per database per run' do + ['backup1', 'backup2'].each do |database| + run_shell("ls -l /tmp/backups/mysql_backup_#{database}_*-*.sql.bz2 | wc -l") do |r| + expect(r.stdout).to match(%r{2}) + expect(r.exit_code).to be_zero + end end end end - # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength end -end -context 'with xtrabackup enabled' do - context 'should work with no errors', if: ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength - pp = <<-MANIFEST + context 'with xtrabackup enabled' do + context 'should work with no errors', if: ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength + pp = <<-MANIFEST class { 'mysql::server': root_password => 'password' } mysql::db { [ 'backup1', 'backup2' ]: user => 'backup', password => 'secret', } case $facts['os']['family'] { /Debian/: { if versioncmp($::operatingsystemmajrelease, '8') >= 0 { $source_url = "http://repo.percona.com/apt/percona-release_1.0-22.generic_all.deb" } else { $source_url = "http://repo.percona.com/apt/percona-release_latest.${facts['os']['distro']['codename']}_all.deb" } file { '/tmp/percona-release_latest.deb': ensure => present, source => $source_url, } ensure_packages('gnupg') ensure_packages('gnupg2') ensure_packages('curl') ensure_packages('percona-release',{ ensure => present, provider => 'dpkg', source => '/tmp/percona-release_latest.deb', notify => Exec['apt-get update'], }) exec { 'apt-get update': path => '/usr/bin:/usr/sbin:/bin:/sbin', refreshonly => true, } } /RedHat/: { # RHEL/CentOS 5 is no longer supported by Percona, but older versions # of the repository are still available. if versioncmp($::operatingsystemmajrelease, '6') >= 0 { $percona_url = 'http://repo.percona.com/yum/percona-release-latest.noarch.rpm' $epel_url = "https://download.fedoraproject.org/pub/epel/epel-release-latest-${facts['os']['release']['major']}.noarch.rpm" } else { $percona_url = 'http://repo.percona.com/yum/release/5/os/noarch/percona-release-0.1-3.noarch.rpm' $epel_url = 'https://archives.fedoraproject.org/pub/archive/epel/epel-release-latest-5.noarch.rpm' } ensure_packages('percona-release',{ ensure => present, provider => 'rpm', source => $percona_url, }) ensure_packages('epel-release',{ ensure => present, provider => 'rpm', source => $epel_url, }) if ($facts['os']['name'] == 'Scientific') { # $releasever resolves to '6.10' instead of '6' which breaks Percona repos file { '/etc/yum/vars/releasever': ensure => present, content => '6', } } } default: { } } class { 'mysql::server::backup': backupuser => 'myuser', backuppassword => 'mypassword', backupdir => '/tmp/xtrabackups', provider => 'xtrabackup', execpath => '/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin', } MANIFEST - it 'when configuring mysql backup' do - idempotent_apply(pp) + it 'when configuring mysql backup' do + idempotent_apply(pp) + end end - end - describe 'xtrabackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') && ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength - before(:all) do - pre_run - end + describe 'xtrabackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') && ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength + before(:all) do + pre_run + end - it 'runs xtrabackup.sh full backup with no errors' do - run_shell('/usr/local/sbin/xtrabackup.sh --target-dir=/tmp/xtrabackups/$(date +%F)_full --backup 2>&1 | tee /tmp/xtrabackup_full.log') do |r| - expect(r.exit_code).to be_zero + it 'runs xtrabackup.sh full backup with no errors' do + run_shell('/usr/local/sbin/xtrabackup.sh --target-dir=/tmp/xtrabackups/$(date +%F)_full --backup 2>&1 | tee /tmp/xtrabackup_full.log') do |r| + expect(r.exit_code).to be_zero + end end - end - it 'xtrabackup reports success for the full backup' do - # NOTE: Once support for CentOS 6 is dropped, we should check for "completed OK" instead. - run_shell('grep "xtrabackup: Transaction log of lsn" /tmp/xtrabackup_full.log') do |r| - expect(r.exit_code).to be_zero + it 'xtrabackup reports success for the full backup' do + # NOTE: Once support for CentOS 6 is dropped, we should check for "completed OK" instead. + run_shell('grep "xtrabackup: Transaction log of lsn" /tmp/xtrabackup_full.log') do |r| + expect(r.exit_code).to be_zero + end end - end - it 'creates a subdirectory for the full backup' do - run_shell('find /tmp/xtrabackups -mindepth 1 -maxdepth 1 -type d -name $(date +%Y)\*full | wc -l') do |r| - expect(r.stdout).to match(%r{1}) - expect(r.exit_code).to be_zero + it 'creates a subdirectory for the full backup' do + run_shell('find /tmp/xtrabackups -mindepth 1 -maxdepth 1 -type d -name $(date +%Y)\*full | wc -l') do |r| + expect(r.stdout).to match(%r{1}) + expect(r.exit_code).to be_zero + end end - end - it 'runs xtrabackup.sh incremental backup with no errors' do - run_shell('sleep 1') - run_shell('/usr/local/sbin/xtrabackup.sh --incremental-basedir=/tmp/xtrabackups/$(date +%F)_full --target-dir=/tmp/xtrabackups/$(date +%F_%H-%M-%S) --backup 2>&1 | tee /tmp/xtrabackup_inc.log') do |r| # rubocop:disable Layout/LineLength - expect(r.exit_code).to be_zero + it 'runs xtrabackup.sh incremental backup with no errors' do + run_shell('sleep 1') + run_shell('/usr/local/sbin/xtrabackup.sh --incremental-basedir=/tmp/xtrabackups/$(date +%F)_full --target-dir=/tmp/xtrabackups/$(date +%F_%H-%M-%S) --backup 2>&1 | tee /tmp/xtrabackup_inc.log') do |r| # rubocop:disable Layout/LineLength + expect(r.exit_code).to be_zero + end end - end - it 'xtrabackup reports success for the incremental backup' do - # NOTE: Once support for CentOS 6 is dropped, we should check for "completed OK" instead. - run_shell('grep "xtrabackup: Transaction log of lsn" /tmp/xtrabackup_inc.log') do |r| - expect(r.exit_code).to be_zero + it 'xtrabackup reports success for the incremental backup' do + # NOTE: Once support for CentOS 6 is dropped, we should check for "completed OK" instead. + run_shell('grep "xtrabackup: Transaction log of lsn" /tmp/xtrabackup_inc.log') do |r| + expect(r.exit_code).to be_zero + end end - end - it 'creates a new subdirectory for each backup' do - run_shell('find /tmp/xtrabackups -mindepth 1 -maxdepth 1 -type d -name $(date +%Y)\* | wc -l') do |r| - expect(r.stdout).to match(%r{2}) - expect(r.exit_code).to be_zero + it 'creates a new subdirectory for each backup' do + run_shell('find /tmp/xtrabackups -mindepth 1 -maxdepth 1 -type d -name $(date +%Y)\* | wc -l') do |r| + expect(r.stdout).to match(%r{2}) + expect(r.exit_code).to be_zero + end end end end - # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength -end -context 'with xtrabackup enabled and incremental backups disabled' do - context 'should work with no errors', if: ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength - pp = <<-MANIFEST + context 'with xtrabackup enabled and incremental backups disabled' do + context 'should work with no errors', if: ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength + pp = <<-MANIFEST class { 'mysql::server': root_password => 'password' } mysql::db { [ 'backup1', 'backup2' ]: user => 'backup', password => 'secret', } case $facts['os']['family'] { /Debian/: { if versioncmp($::operatingsystemmajrelease, '8') >= 0 { $source_url = "http://repo.percona.com/apt/percona-release_1.0-22.generic_all.deb" } else { $source_url = "http://repo.percona.com/apt/percona-release_latest.${facts['os']['distro']['codename']}_all.deb" } file { '/tmp/percona-release_latest.deb': ensure => present, source => $source_url, } ensure_packages('gnupg') ensure_packages('gnupg2') ensure_packages('percona-release',{ ensure => present, provider => 'dpkg', source => '/tmp/percona-release_latest.deb', notify => Exec['apt-get update'], }) exec { 'apt-get update': path => '/usr/bin:/usr/sbin:/bin:/sbin', refreshonly => true, } } /RedHat/: { # RHEL/CentOS 5 is no longer supported by Percona, but older versions # of the repository are still available. if versioncmp($::operatingsystemmajrelease, '6') >= 0 { $percona_url = 'http://repo.percona.com/yum/percona-release-latest.noarch.rpm' $epel_url = "https://download.fedoraproject.org/pub/epel/epel-release-latest-${facts['os']['release']['major']}.noarch.rpm" } else { $percona_url = 'http://repo.percona.com/yum/release/5/os/noarch/percona-release-0.1-3.noarch.rpm' $epel_url = 'https://archives.fedoraproject.org/pub/archive/epel/epel-release-latest-5.noarch.rpm' } ensure_packages('percona-release',{ ensure => present, provider => 'rpm', source => $percona_url, }) ensure_packages('epel-release',{ ensure => present, provider => 'rpm', source => $epel_url, }) if ($facts['os']['name'] == 'Scientific') { # $releasever resolves to '6.10' instead of '6' which breaks Percona repos file { '/etc/yum/vars/releasever': ensure => present, content => '6', } } } default: { } } class { 'mysql::server::backup': backupuser => 'myuser', backuppassword => 'mypassword', backupdir => '/tmp/xtrabackups', provider => 'xtrabackup', incremental_backups => false, execpath => '/usr/bin:/usr/sbin:/bin:/sbin:/opt/zimbra/bin', } MANIFEST - it 'when configuring mysql backup' do - idempotent_apply(pp) + it 'when configuring mysql backup' do + idempotent_apply(pp) + end end - end - describe 'xtrabackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') && ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength - before(:all) do - pre_run - end + describe 'xtrabackup.sh', if: Gem::Version.new(mysql_version) < Gem::Version.new('5.7.0') && ((os[:family] == 'debian' && os[:release].to_i >= 8) || (os[:family] == 'ubuntu' && os[:release] =~ %r{^16\.04|^18\.04}) || (os[:family] == 'redhat' && os[:release].to_i > 6)) do # rubocop:disable Layout/LineLength + before(:all) do + pre_run + end - it 'runs xtrabackup.sh with no errors' do - run_shell('/usr/local/sbin/xtrabackup.sh --target-dir=/tmp/xtrabackups/$(date +%F_%H-%M-%S) --backup 2>&1 | tee /tmp/xtrabackup.log') do |r| - expect(r.exit_code).to be_zero + it 'runs xtrabackup.sh with no errors' do + run_shell('/usr/local/sbin/xtrabackup.sh --target-dir=/tmp/xtrabackups/$(date +%F_%H-%M-%S) --backup 2>&1 | tee /tmp/xtrabackup.log') do |r| + expect(r.exit_code).to be_zero + end end - end - it 'xtrabackup reports success for the backup' do - # NOTE: Once support for CentOS 6 is dropped, we should check for "completed OK" instead. - run_shell('grep "xtrabackup: Transaction log of lsn" /tmp/xtrabackup.log') do |r| - expect(r.exit_code).to be_zero + it 'xtrabackup reports success for the backup' do + # NOTE: Once support for CentOS 6 is dropped, we should check for "completed OK" instead. + run_shell('grep "xtrabackup: Transaction log of lsn" /tmp/xtrabackup.log') do |r| + expect(r.exit_code).to be_zero + end end end end - # rubocop:enable RSpec/MultipleExpectations, RSpec/ExampleLength end diff --git a/spec/classes/mysql_server_account_security_spec.rb b/spec/classes/mysql_server_account_security_spec.rb index 040dddd..3750f5c 100644 --- a/spec/classes/mysql_server_account_security_spec.rb +++ b/spec/classes/mysql_server_account_security_spec.rb @@ -1,85 +1,85 @@ # frozen_string_literal: true require 'spec_helper' describe 'mysql::server::account_security' do on_supported_os.each do |os, facts| context "on #{os}" do let(:pre_condition) do <<-EOF anchor {'mysql::server::end': } EOF end context 'with fqdn==myhost.mydomain' do let(:facts) do facts.merge(root_home: '/root', fqdn: 'myhost.mydomain', hostname: 'myhost') end ['root@myhost.mydomain', 'root@127.0.0.1', 'root@::1', '@myhost.mydomain', '@localhost', '@%'].each do |user| - it "removes Mysql_User[#{user}]" do # rubocop:disable RSpec/RepeatedExample + it "removes Mysql_User[#{user}]" do # rubocop:disable RSpec/RepeatedExample,RSpec/RepeatedDescription is_expected.to contain_mysql_user(user).with_ensure('absent') end end # When the hostname doesn't match the fqdn we also remove these. # We don't need to test the inverse as when they match they are # covered by the above list. ['root@myhost', '@myhost'].each do |user| - it "removes Mysql_User[#{user}]" do # rubocop:disable RSpec/RepeatedExample + it "removes Mysql_User[#{user}]" do # rubocop:disable RSpec/RepeatedExample,RSpec/RepeatedDescription is_expected.to contain_mysql_user(user).with_ensure('absent') end end it 'removes Mysql_database[test]' do is_expected.to contain_mysql_database('test').with_ensure('absent') end end context 'with fqdn==localhost' do let(:facts) do facts.merge(root_home: '/root', fqdn: 'localhost', hostname: 'localhost') end ['root@127.0.0.1', 'root@::1', '@localhost', 'root@localhost.localdomain', '@localhost.localdomain', '@%'].each do |user| it "removes Mysql_User[#{user}] for fqdn==localhost" do is_expected.to contain_mysql_user(user).with_ensure('absent') end end end context 'with fqdn==localhost.localdomain' do let(:facts) do facts.merge(root_home: '/root', fqdn: 'localhost.localdomain', hostname: 'localhost') end ['root@127.0.0.1', 'root@::1', '@localhost', 'root@localhost.localdomain', '@localhost.localdomain', '@%'].each do |user| it "removes Mysql_User[#{user}] for fqdn==localhost.localdomain" do is_expected.to contain_mysql_user(user).with_ensure('absent') end end end end end end diff --git a/spec/unit/puppet/provider/mysql_user/mysql_spec.rb b/spec/unit/puppet/provider/mysql_user/mysql_spec.rb index 2773d2e..ba72cc1 100644 --- a/spec/unit/puppet/provider/mysql_user/mysql_spec.rb +++ b/spec/unit/puppet/provider/mysql_user/mysql_spec.rb @@ -1,496 +1,497 @@ # frozen_string_literal: true require 'spec_helper' describe Puppet::Type.type(:mysql_user).provider(:mysql) do # Output of mysqld -V mysql_version_string_hash = { 'mysql-5.5' => { version: '5.5.46', string: '/usr/sbin/mysqld Ver 5.5.46-log for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mysql-5.6' => { version: '5.6.27', string: '/usr/sbin/mysqld Ver 5.6.27 for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mysql-5.7.1' => { version: '5.7.1', string: '/usr/sbin/mysqld Ver 5.7.1 for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mysql-5.7.6' => { version: '5.7.8', string: '/usr/sbin/mysqld Ver 5.7.8-rc for Linux on x86_64 (MySQL Community Server (GPL))', mysql_type: 'mysql', }, 'mariadb-10.0' => { version: '10.0.21', string: '/usr/sbin/mysqld Ver 10.0.21-MariaDB for Linux on x86_64 (MariaDB Server)', mysql_type: 'mariadb', }, 'mariadb-10.0-deb8' => { version: '10.0.23', string: '/usr/sbin/mysqld (mysqld 10.0.23-MariaDB-0+deb8u1)', mysql_type: 'mariadb', }, 'mariadb-10.1.44' => { version: '10.1.44', string: '/usr/sbin/mysqld (mysqld 10.1.44-MariaDB-1~bionic)', mysql_type: 'mariadb', }, 'mariadb-10.3.22' => { version: '10.3.22', string: '/usr/sbin/mysqld (mysqld 10.3.22-MariaDB-0+deb10u1)', mysql_type: 'mariadb', }, 'percona-5.5' => { version: '5.5.39', string: 'mysqld Ver 5.5.39-36.0-55 for Linux on x86_64 (Percona XtraDB Cluster (GPL), Release rel36.0, Revision 824, WSREP version 25.11, wsrep_25.11.r4023)', mysql_type: 'percona', }, } let(:defaults_file) { '--defaults-extra-file=/root/.my.cnf' } let(:system_database) { '--database=mysql' } let(:newhash) { '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' } let(:raw_users) do <<-SQL_OUTPUT root@127.0.0.1 root@::1 @localhost debian-sys-maint@localhost root@localhost usvn_user@localhost @vagrant-ubuntu-raring-64 SQL_OUTPUT # rubocop:enable Layout/IndentHeredoc end let(:parsed_users) { ['root@127.0.0.1', 'root@::1', '@localhost', 'debian-sys-maint@localhost', 'root@localhost', 'usvn_user@localhost', '@vagrant-ubuntu-raring-64'] } let(:provider) { resource.provider } let(:instance) { provider.class.instances.first } let(:resource) do Puppet::Type.type(:mysql_user).new( ensure: :present, password_hash: '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4', name: 'joe@localhost', max_user_connections: '10', max_connections_per_hour: '10', max_queries_per_hour: '10', max_updates_per_hour: '10', provider: described_class.name, ) end before :each do # Set up the stubs for an instances call. Facter.stubs(:value).with(:root_home).returns('/root') Facter.stubs(:value).with(:mysql_version).returns('5.6.24') provider.class.instance_variable_set(:@mysqld_version_string, '5.6.24') Puppet::Util.stubs(:which).with('mysql').returns('/usr/bin/mysql') Puppet::Util.stubs(:which).with('mysqld').returns('/usr/sbin/mysqld') File.stubs(:file?).with('/root/.my.cnf').returns(true) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns('joe@localhost') provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = 'joe@localhost'", 'regular').returns('10 10 10 10 *6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4') # rubocop:disable Layout/LineLength end describe 'self.instances' do it 'returns an array of users MySQL 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users MySQL 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users MySQL >= 5.7.0 < 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users MySQL >= 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, AUTHENTICATION_STRING, PLUGIN FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users mariadb 10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users mariadb >= 10.1.21' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD, PLUGIN, AUTHENTICATION_STRING FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end it 'returns an array of users percona 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string]) provider.class.stubs(:mysql_caller).with("SELECT CONCAT(User, '@',Host) AS User FROM mysql.user", 'regular').returns(raw_users) parsed_users.each { |user| provider.class.stubs(:mysql_caller).with("SELECT MAX_USER_CONNECTIONS, MAX_CONNECTIONS, MAX_QUESTIONS, MAX_UPDATES, SSL_TYPE, SSL_CIPHER, X509_ISSUER, X509_SUBJECT, PASSWORD /*!50508 , PLUGIN */ FROM mysql.user WHERE CONCAT(user, '@', host) = '#{user}'", 'regular').returns('10 10 10 10 ') } # rubocop:disable Layout/LineLength usernames = provider.class.instances.map { |x| x.name } expect(parsed_users).to match_array(usernames) end end describe 'mysql version and type detection' do mysql_version_string_hash.each do |_name, line| version = line[:version] string = line[:string] mysql_type = line[:mysql_type] it "detects version '#{version}'" do provider.class.instance_variable_set(:@mysqld_version_string, string) expect(provider.mysqld_version).to eq(version) end it "detects type '#{mysql_type}'" do provider.class.instance_variable_set(:@mysqld_version_string, string) expect(provider.mysqld_type).to eq(mysql_type) end end end describe 'self.prefetch' do it 'exists' do provider.class.instances provider.class.prefetch({}) end end describe 'create' do it 'makes a user' do provider.class.expects(:mysql_caller).with("CREATE USER 'joe'@'localhost' IDENTIFIED BY PASSWORD '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system') provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10", 'system') # rubocop:disable Layout/LineLength provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system') provider.expects(:exists?).returns(true) expect(provider.create).to be_truthy end it 'creates a user using IF NOT EXISTS' do provider.class.instance_variable_set(:@mysqld_version_string, '5.7.6') - provider.class.expects(:mysql_caller).with("CREATE USER IF NOT EXISTS 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system') provider.class.expects(:mysql_caller).with("ALTER USER IF EXISTS 'joe'@'localhost' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10", 'system') # rubocop:disable Layout/LineLength + provider.class.expects(:mysql_caller).with("CREATE USER IF NOT EXISTS 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system') + provider.class.expects(:mysql_caller).with("ALTER USER IF EXISTS 'joe'@'localhost' WITH MAX_USER_CONNECTIONS 10 MAX_CONNECTIONS_PER_HOUR 10 MAX_QUERIES_PER_HOUR 10 MAX_UPDATES_PER_HOUR 10", 'system') # rubocop:disable Layout/LineLength provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' REQUIRE NONE", 'system') provider.expects(:exists?).returns(true) expect(provider.create).to be_truthy end end describe 'destroy' do it 'removes a user if present' do provider.class.expects(:mysql_caller).with("DROP USER 'joe'@'localhost'", 'system') provider.expects(:exists?).returns(false) expect(provider.destroy).to be_truthy end it 'removes a user using IF EXISTS' do provider.class.instance_variable_set(:@mysqld_version_string, '5.7.1') provider.class.expects(:mysql_caller).with("DROP USER IF EXISTS 'joe'@'localhost'", 'system') expect(provider.destroy).to be_truthy end end describe 'exists?' do it 'checks if user exists' do expect(instance).to be_exists end end describe 'self.mysqld_version' do it 'uses the mysqld_version fact if unset' do provider.class.instance_variable_set(:@mysqld_version_string, nil) Facter.stubs(:value).with(:mysqld_version).returns('5.6.24') expect(provider.mysqld_version).to eq '5.6.24' end it 'returns 5.7.6 for "mysqld Ver 5.7.6 for Linux on x86_64 (MySQL Community Server (GPL))"' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.6 for Linux on x86_64 (MySQL Community Server (GPL))') expect(provider.mysqld_version).to eq '5.7.6' end it 'returns 5.7.6 for "mysqld Ver 5.7.6-rc for Linux on x86_64 (MySQL Community Server (GPL))"' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.6-rc for Linux on x86_64 (MySQL Community Server (GPL))') expect(provider.mysqld_version).to eq '5.7.6' end it 'detects >= 5.7.6 for 5.7.7-log' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.7-log for Linux on x86_64 (MySQL Community Server (GPL))') expect(Puppet::Util::Package.versioncmp(provider.mysqld_version, '5.7.6')).to be >= 0 end it 'detects < 5.7.6 for 5.7.5-log' do provider.class.instance_variable_set(:@mysqld_version_string, 'mysqld Ver 5.7.5-log for Linux on x86_64 (MySQL Community Server (GPL))') expect(Puppet::Util::Package.versioncmp(provider.mysqld_version, '5.7.6')).to be < 0 end end describe 'self.defaults_file' do it 'sets --defaults-extra-file' do File.stubs(:file?).with('/root/.my.cnf').returns(true) expect(provider.defaults_file).to eq '--defaults-extra-file=/root/.my.cnf' end it 'fails if file missing' do File.expects(:file?).with('/root/.my.cnf').returns(false) expect(provider.defaults_file).to be_nil end end describe 'password_hash' do it 'returns a hash' do expect(instance.password_hash).to eq('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4') end end describe 'password_hash=' do it 'changes the hash mysql 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash mysql 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash mysql < 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash MySQL >= 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH mysql_native_password AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash mariadb-10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end it 'changes the hash to an ed25519 hash mariadb >= 10.1.21 and < 10.2.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) resource.stubs(:value).with(:plugin).returns('ed25519') provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET password = '', plugin = 'ed25519', authentication_string = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' where CONCAT(user, '@', host) = 'joe@localhost'; FLUSH PRIVILEGES", 'system').returns('0') # rubocop:disable Layout/LineLength provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' end it 'changes the hash to an ed25519 hash mariadb >= 10.2.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.3.22'][:string]) resource.stubs(:value).with(:plugin).returns('ed25519') provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH ed25519 AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') provider.expects(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') provider.password_hash = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' end it 'changes the hash to an invalid ed25519 hash mariadb >= 10.1.21' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) resource.stubs(:value).with(:plugin).returns('ed25519') expect { provider.password_hash = 'invalid' }.to raise_error(ArgumentError, 'ed25519 hash should be 43 bytes long.') end it 'changes the hash percona-5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['percona-5.5'][:string]) provider.class.expects(:mysql_caller).with("SET PASSWORD FOR 'joe'@'localhost' = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5'", 'system').returns('0') provider.expects(:password_hash).returns('*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5') provider.password_hash = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF5' end end describe 'plugin=' do context 'auth_socket' do context 'MySQL < 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET plugin = 'auth_socket', password = '' WHERE CONCAT(user, '@', host) = 'joe@localhost'", 'system').returns('0') provider.expects(:plugin).returns('auth_socket') provider.plugin = 'auth_socket' end end context 'MySQL >= 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'auth_socket'", 'system').returns('0') provider.expects(:plugin).returns('auth_socket') provider.plugin = 'auth_socket' end end end context 'mysql_native_password' do context 'MySQL < 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET plugin = 'mysql_native_password', password = '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4' WHERE CONCAT(user, '@', host) = 'joe@localhost'", 'system').returns('0') # rubocop:disable Layout/LineLength provider.expects(:plugin).returns('mysql_native_password') provider.plugin = 'mysql_native_password' end end context 'MySQL >= 5.7.6' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'mysql_native_password' AS '*6C8989366EAF75BB670AD8EA7A7FC1176A95CEF4'", 'system').returns('0') provider.expects(:plugin).returns('mysql_native_password') provider.plugin = 'mysql_native_password' end end end context 'ed25519' do context 'mariadb >= 10.1.21 and < 10.2.0' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.1.44'][:string]) resource.stubs('[]').with(:name).returns('joe@localhost') resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') provider.class.expects(:mysql_caller).with("UPDATE mysql.user SET password = '', plugin = 'ed25519', authentication_string = 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU' where CONCAT(user, '@', host) = 'joe@localhost'; FLUSH PRIVILEGES", 'system').returns('0') # rubocop:disable Layout/LineLength provider.expects(:plugin).returns('ed25519') provider.plugin = 'ed25519' end end context 'mariadb >= 10.2.0' do it 'changes the authentication plugin' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.3.22'][:string]) resource.stubs('[]').with(:name).returns('joe@localhost') resource.stubs('[]').with(:password_hash).returns('z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU') provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' IDENTIFIED WITH 'ed25519' AS 'z0pjExBYbzbupUByZRrQvC6kRCcE8n/tC7kUdUD11fU'", 'system').returns('0') provider.expects(:plugin).returns('ed25519') provider.plugin = 'ed25519' end end end # rubocop:enable RSpec/NestedGroups end describe 'tls_options=' do it 'adds SSL option grant in mysql 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').returns('0') provider.expects(:tls_options).returns(['NONE']) provider.tls_options = ['NONE'] end it 'adds SSL option grant in mysql 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').returns('0') provider.expects(:tls_options).returns(['NONE']) provider.tls_options = ['NONE'] end it 'adds SSL option grant in mysql < 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').returns('0') provider.expects(:tls_options).returns(['NONE']) provider.tls_options = ['NONE'] end it 'adds SSL option grant in mysql >= 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' REQUIRE NONE", 'system').returns('0') provider.expects(:tls_options).returns(['NONE']) provider.tls_options = ['NONE'] end it 'adds SSL option grant in mariadb-10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE NONE", 'system').returns('0') provider.expects(:tls_options).returns(['NONE']) provider.tls_options = ['NONE'] end end describe 'tls_options=required' do it 'adds mTLS option grant in mysql 5.5' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.5'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS option grant in mysql 5.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.6'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS option grant in mysql < 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.1'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS option grant in mysql >= 5.7.6' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mysql-5.7.6'][:string]) provider.class.expects(:mysql_caller).with("ALTER USER 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end it 'adds mTLS option grant in mariadb-10.0' do provider.class.instance_variable_set(:@mysqld_version_string, mysql_version_string_hash['mariadb-10.0'][:string]) provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' REQUIRE ISSUER '/CN=Certificate Authority' AND SUBJECT '/OU=MySQL Users/CN=Username'", 'system').returns('0') provider.expects(:tls_options).returns(['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\'']) provider.tls_options = ['ISSUER \'/CN=Certificate Authority\'', 'SUBJECT \'/OU=MySQL Users/CN=Username\''] end end ['max_user_connections', 'max_connections_per_hour', 'max_queries_per_hour', 'max_updates_per_hour'].each do |property| describe property do it "returns #{property}" do expect(instance.send(property.to_s.to_sym)).to eq('10') end end describe "#{property}=" do it "changes #{property}" do provider.class.expects(:mysql_caller).with("GRANT USAGE ON *.* TO 'joe'@'localhost' WITH #{property.upcase} 42", 'system').returns('0') provider.expects(property.to_sym).returns('42') provider.send("#{property}=".to_sym, '42') end end end end