Projects
home:rottame:rubygems
rubygem-ygg_provisioner
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 8
View file
rubygem-ygg_provisioner.changes
Changed
@@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Mon Dec 1 16:51:03 UTC 2025 - Angelo Grossini <rottame@intercom.it> + +- update to 1.5.0 + +------------------------------------------------------------------- Tue Sep 12 09:04:44 UTC 2023 - Angelo Grossini <rottame@intercom.it> - revert test
View file
rubygem-ygg_provisioner.spec
Changed
@@ -1,16 +1,20 @@ %define mod_name ygg_provisioner %define mod_full_name %{mod_name}-%{version} Name: rubygem-ygg_provisioner -Version: 1.4.1 +Version: 1.5.0 Release: 0 Summary: Yggdra base provisioner License: Apache-2.0 Group: Development/Languages/Ruby URL: https://intercom.it Source: https://gems.intercom.it/gems/%{mod_full_name}.gem +Source1: gem2rpm.yml BuildRequires: %{ruby >= 1.9} BuildRequires: %{rubygem gem2rpm} BuildRequires: ruby-macros >= 5 +Suggests: %{rubygem sqlite3} +Suggests: %{rubygem kyotocabinet} +Suggests: %{rubygem lmdb} %description Yggdra base provisioner @@ -21,6 +25,8 @@ %install %gem_install \ + --symlink-binaries \ + --no-ri --no-rdoc \ --doc-files="README" \ -f
View file
gem2rpm.yml
Added
@@ -0,0 +1,4 @@ +--- +:disable_docs: true +:patches: +:sources: \ No newline at end of file
View file
ygg_provisioner-1.4.1.gem/data/.gitignore
Deleted
@@ -1,1 +0,0 @@ -pkg/
View file
ygg_provisioner-1.4.1.gem/data/.gitlab-ci.yml
Deleted
@@ -1,19 +0,0 @@ -stages: - - build-gem - -build: - stage: build-gem - #rules: - # - if: '$CI_COMMIT_BRANCH == "deploy"' - # when: always - # - if: '$CI_COMMIT_BRANCH == "staging"' - # when: always - # - when: never - image: ruby:2.4 - script: | - mkdir -p $HOME/.gem - touch $HOME/.gem/credentials - chmod 0600 $HOME/.gem/credentials - printf -- "---\n:rubygems_api_key: Bearer ${GEMINABOX_API_KEY}\n" > $HOME/.gem/credentials - gem build *.gemspec --output=release.gem - gem push release.gem --host https://gems.intercom.it
View file
ygg_provisioner-1.4.1.gem/data/lib/ygg/provisioner/colon_delimited_file.rb
Deleted
@@ -1,67 +0,0 @@ -# -# Copyright (C) 2013-2013, Intercom Srl, Daniele Orlandi -# -# Author:: Daniele Orlandi <daniele@orlandi.com> -# Lele Forzani <lele@windmill.it> -# Alfredo Cerutti <acerutti@intercom.it> -# -# License:: You can redistribute it and/or modify it under the terms of the LICENSE file. -# - -require 'ygg/provisioner/file_switcher' - -module Ygg -module Provisioner - -class ColonDelimitedFile - include FileSwitcher - - def initialize(filename, opts = {}) - @entries = {} - @tmp_filename = opts:tmpname || filename + '.new' - - File::open(filename, 'r') do |f| - f.each_line do |line| - line.chomp! - entry = line.split(':', opts:limit || -1) - - @entriesentry0 = entry - end - end rescue Errno::ENOENT - - yield self - - File::open(@tmp_filename, 'w') do |f| - @entries.each do |entrykey, entry| - f << entry.join(':') + "\n" - end - end - - uid = opts:uid - gid = opts:gid - begin - uid = File.stat(filename).uid - gid = File.stat(filename).gid - rescue Errno::ENOENT - end - - FileUtils::chown(uid, gid, @tmp_filename) - - switch_files(@tmp_filename, filename) - end - - def set(data) - @entriesdata0 = data - end - - def (key) - @entrieskey - end - - def delete(key) - @entries.delete(key) - end -end - -end -end
View file
ygg_provisioner-1.4.1.gem/data/lib/ygg/provisioner/file_switcher.rb
Deleted
@@ -1,23 +0,0 @@ -# -# Copyright (C) 2013-2013, Intercom Srl, Daniele Orlandi -# -# Author:: Daniele Orlandi <daniele@orlandi.com> -# Lele Forzani <lele@windmill.it> -# Alfredo Cerutti <acerutti@intercom.it> -# -# License:: You can redistribute it and/or modify it under the terms of the LICENSE file. -# - -module Ygg -module Provisioner - -module FileSwitcher - def switch_files(f1, f2) - begin ; File.unlink(f2 + '.old') rescue Errno::ENOENT ; end - begin ; File.link(f2, f2 + '.old') rescue Errno::ENOENT ; end - File.rename(f1, f2) - end -end - -end -end
View file
ygg_provisioner-1.4.1.gem/data/lib/ygg/provisioner/tools.rb
Deleted
@@ -1,179 +0,0 @@ -# -# Copyright (C) 2013-2013, Intercom Srl, Daniele Orlandi -# -# Author:: Daniele Orlandi <daniele@orlandi.com> -# Lele Forzani <lele@windmill.it> -# Alfredo Cerutti <acerutti@intercom.it> -# -# License:: You can redistribute it and/or modify it under the terms of the LICENSE file. -# - -require 'fiber' -require 'publisher' -require 'eventmachine' - - - - - - - - -module EventMachine - def self.popen3(*args) - new_stderr = $stderr.dup - rd, wr = IO::pipe - $stderr.reopen wr - connection = EM.popen(*args) - $stderr.reopen new_stderr - EM.attach rd, Popen3StderrHandler, connection - connection - end - - class Popen3StderrHandler < EventMachine::Connection - def initialize(connection) - @connection = connection - end - - def receive_data(data) - @connection.receive_stderr(data) - end - end -end - - -module Ygg -module Provisioner - -module Tools - def switch_files(f1, f2) - begin ; File.unlink(f2 + '.old') rescue Errno::ENOENT ; end - begin ; File.link(f2, f2 + '.old') rescue Errno::ENOENT ; end - File.rename(f1, f2) - end - - class CommandError < StandardError - attr_accessor :status - - def initialize(msg, status) - super msg - self.status = status - end - end - - class CommandRunner < EventMachine::Connection - extend Publisher - - can_fire :success, :failure, :receive_data, :receive_stderr - - # @private - def initialize(collect_data = true) - super - @out = '' - @collect_data = collect_data - end - - def self.open(cmd, collect_data = true) - EventMachine.popen3(cmd, self, collect_data) - end - - def receive_data(data) - @out += data if @collect_data - fire :receive_data, data - end - - def receive_stderr(data) - fire :receive_stderr, data - end - - def unbind - if get_status.success? - fire :success, @out, get_status - else - fire :failure, @out, get_status - end - end - end - - def run_command(cmd, opts = {}, &block) - if block - run_command_async(cmd, opts, &block) - - nil - else - fiber = Fiber.current - - run_command(cmd, opts) do |out, status| - fiber.resume(out, status) - end - - (out, status) = Fiber.yield - - if opts:with_status - out, status - else - raise CommandError.new('Error running command', status) if !status.success? - out - end - end - end - - #private - def run_command_async(cmd, opts = {}, &block) - out = '' - - plog = respond_to?(:plog) ? self.plog : opts:plog - - old_euid = Process::UID.eid - old_egid = Process::GID.eid - - if opts:uid - # This isn't the proper way to drop the privileges for the child process!!! FIXME TODO - Process::UID.grant_privilege(opts:uid) - end - - if opts:env - # This isn't the proper way to set the environment for the child process!!! FIXME TODO - opts:env.each { |k,v| ENVk = v } - end - - plog.debug " = running command #{cmd.inspect}" if plog - - c = CommandRunner.open(cmd, !opts:progress) - - if opts:send_data - c.send_data(opts:send_data) - end - - if opts:progress - c.on(:receive_data) do |out| - yield out, nil if block_given? - end - - c.on(:receive_stderr) do |out| - opts:stderr_handler.call(out) if opts:stderr_handler - end - end - - c.on(:success) do |out, result| - out.each_line { |l| plog.info l } if plog - - yield out, result if block_given? - end - - c.on(:failure) do |out, result| - out.each_line { |l| plog.error l } if plog - - yield out, result if block_given? - end - - Process::UID.eid = old_euid - Process::GID.eid = old_egid - - nil - end - -end - -end -end
View file
ygg_provisioner-1.4.1.gem/checksums.yaml.gz -> ygg_provisioner-1.5.0.gem/checksums.yaml.gz
Changed
@@ -1,7 +1,7 @@ --- SHA256: - metadata.gz: d9f2873d1c242bb177ab78e0ce269baad4451ef98fefae6a376c2be69e90aded - data.tar.gz: 7d90b7f4e3b078a59f097980858b8fce13022c44b461725a347169f1673d4585 + metadata.gz: 5848ee3c07f250b2417e0f0e7b759eb1fbaea2fe0da60c7c2342f47144a0014b + data.tar.gz: 342c05c1df38b2aa6c5f47677c0dd1fdf471e77f2b726ddd1b088396ba4fb1fd SHA512: - metadata.gz: ed016c953e3f2f9a5fcf5ff4dd2e0dc5fb5e1c3a19f8e62236fed518914fa0935cedbf3c797ac05f0298958f5953de3e078fdfdcc083573088eb318a38dce61a - data.tar.gz: 04b47f1fbb486eb8cbcd5adf21166308e8607b347de242a8ae70f256796e304bba8b15c9547b4db221d7bc8c4ae739850197541860b5373fccb7fafac8a9cd15 + metadata.gz: 17926a8885236290a164fee4d4febc8e68471a716af68df57e18842740a4e50c32ec0948b1c06b0da58811e9bf18ce9259313aebde7999eeeefb0b6add5fb93d + data.tar.gz: 5e5027fda497785ee720a6e591d4d88eca6e59dacb2ddce45da9f64d5b8a70374aae8a14d9b791a95d4de99b58cab60b5a91bdd6f05e478ca713f6b3725e456c
View file
ygg_provisioner-1.4.1.gem/data/Gemfile -> ygg_provisioner-1.5.0.gem/data/Gemfile
Changed
@@ -4,3 +4,6 @@ # Specify your gem's dependencies in a.gemspec gemspec +gem 'kyotocabinet' +gem 'lmdb' +gem 'sqlite3'
View file
ygg_provisioner-1.5.0.gem/data/Gemfile.lock
Added
@@ -0,0 +1,104 @@ +PATH + remote: . + specs: + ygg_provisioner (1.5.0) + eventmachine (~> 1.2.3) + publisher + tomte-agents + +GEM + remote: http://rubygems.org/ + remote: http://gems.intercom.it/ + specs: + activesupport (6.0.6.1) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) + amq-protocol (2.3.0) + amqp (1.8.0) + amq-protocol (>= 2.2.0) + eventmachine + awesome_print (1.9.2) + bson (1.12.5) + bson_ext (1.12.5) + bson (~> 1.12.5) + byebug (11.1.3) + case (0.5.2.1) + coderay (1.1.3) + concurrent-ruby (1.3.5) + diff-lcs (1.3) + eventmachine (1.2.5) + i18n (1.14.7) + concurrent-ruby (~> 1.0) + json (2.7.6) + kyotocabinet (1.33) + lmdb (0.6.1) + method_source (1.1.0) + mime-types (3.3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) + minitest (5.15.0) + pry (0.14.2) + coderay (~> 1.1) + method_source (~> 1.0) + publisher (1.1.2) + rake (12.3.3) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.10.0) + rspec-support (3.10.2) + sqlite3 (1.3.9) + thor (0.20.3) + thread_safe (0.3.6) + tomte-agents (1.1.1) + tomte-core (>= 1.2.0) + tomte-protocol (>= 1.1.1) + tomte-core (1.2.2) + activesupport (>= 5.0.0) + bson (>= 1.12.5) + bson_ext (>= 1.12.5) + case (>= 0.5.2) + json (>= 2.2.0) + mime-types + pry (>= 0.12.2) + rake + thor (>= 0.20.3) + uuidtools (>= 2.1.5) + wml-compat (>= 0.8.6) + tomte-protocol (1.2.3) + amqp (>= 1.8.0) + tomte-core (>= 1.2.0) + tzinfo (1.2.7) + thread_safe (~> 0.1) + uuidtools (3.0.0) + wml-compat (0.8.13) + awesome_print (>= 1.1.0) + uuidtools (>= 2.1.0) + zeitwerk (2.2.2) + +PLATFORMS + x86_64-linux + +DEPENDENCIES + byebug + kyotocabinet + lmdb + pry + rake + rspec + sqlite3 + ygg_provisioner! + +BUNDLED WITH + 2.2.34
View file
ygg_provisioner-1.5.0.gem/data/bin/ygg_provisioner_convert_store
Added
@@ -0,0 +1,47 @@ +#!/usr/bin/ruby + +require 'rubygems' + +require 'active_support/core_ext' +require 'ygg/provisioner/model_store' + +module Ygg +module Provisioner + + # Simple CLI utility to migrate all records from one store to another. + # + # Usage: + # ygg_provisioner_convert_store SOURCE_PATH DESTINATION_PATH + # + # Both SOURCE_PATH and DESTINATION_PATH are opaque specifications understood + # by {Ygg::Provisioner::ModelStore.instantiate}, typically filesystem paths + # such as: + # - /path/to/db.kch + # - /path/to/db.sqlite3 + class StoreConverter + def self.run(argv) + unless argv.size == 2 + $stderr.puts "Usage: #{$0} SOURCE DESTINATION" + exit 1 + end + + source_spec, dest_spec = argv + + source = ModelStore.instantiate(source_spec) + dest = ModelStore.instantiate(dest_spec) + + migrated = 0 + + source.each_pair do |key, value| + dest.put(key, value) + migrated += 1 + end + + $stdout.puts "Migrated #{migrated} records from #{source_spec.inspect} to #{dest_spec.inspect}" + end + end + +end +end + +Ygg::Provisioner::StoreConverter.run(ARGV)
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/internals/colon_delimited_file.rb
Added
@@ -0,0 +1,69 @@ +# +# Copyright (C) 2013-2013, Intercom Srl, Daniele Orlandi +# +# Author:: Daniele Orlandi <daniele@orlandi.com> +# Lele Forzani <lele@windmill.it> +# Alfredo Cerutti <acerutti@intercom.it> +# +# License:: You can redistribute it and/or modify it under the terms of the LICENSE file. +# + +require 'ygg/provisioner/internals/file_switcher' + +module Ygg +module Provisioner +module Internals + +class ColonDelimitedFile + include FileSwitcher + + def initialize(filename, opts = {}) + @entries = {} + @tmp_filename = opts:tmpname || filename + '.new' + + File::open(filename, 'r') do |f| + f.each_line do |line| + line.chomp! + entry = line.split(':', opts:limit || -1) + + @entriesentry0 = entry + end + end rescue Errno::ENOENT + + yield self + + File::open(@tmp_filename, 'w') do |f| + @entries.each do |entrykey, entry| + f << entry.join(':') + "\n" + end + end + + uid = opts:uid + gid = opts:gid + begin + uid = File.stat(filename).uid + gid = File.stat(filename).gid + rescue Errno::ENOENT + end + + FileUtils::chown(uid, gid, @tmp_filename) + + switch_files(@tmp_filename, filename) + end + + def set(data) + @entriesdata0 = data + end + + def (key) + @entrieskey + end + + def delete(key) + @entries.delete(key) + end +end + +end +end +end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/internals/file_switcher.rb
Added
@@ -0,0 +1,25 @@ +# +# Copyright (C) 2013-2013, Intercom Srl, Daniele Orlandi +# +# Author:: Daniele Orlandi <daniele@orlandi.com> +# Lele Forzani <lele@windmill.it> +# Alfredo Cerutti <acerutti@intercom.it> +# +# License:: You can redistribute it and/or modify it under the terms of the LICENSE file. +# + +module Ygg +module Provisioner +module Internals + +module FileSwitcher + def switch_files(f1, f2) + begin ; File.unlink(f2 + '.old') rescue Errno::ENOENT ; end + begin ; File.link(f2, f2 + '.old') rescue Errno::ENOENT ; end + File.rename(f1, f2) + end +end + +end +end +end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/internals/tools.rb
Added
@@ -0,0 +1,181 @@ +# +# Copyright (C) 2013-2013, Intercom Srl, Daniele Orlandi +# +# Author:: Daniele Orlandi <daniele@orlandi.com> +# Lele Forzani <lele@windmill.it> +# Alfredo Cerutti <acerutti@intercom.it> +# +# License:: You can redistribute it and/or modify it under the terms of the LICENSE file. +# + +require 'fiber' +require 'publisher' +require 'eventmachine' + + + + + + + + +module EventMachine + def self.popen3(*args) + new_stderr = $stderr.dup + rd, wr = IO::pipe + $stderr.reopen wr + connection = EM.popen(*args) + $stderr.reopen new_stderr + EM.attach rd, Popen3StderrHandler, connection + connection + end + + class Popen3StderrHandler < EventMachine::Connection + def initialize(connection) + @connection = connection + end + + def receive_data(data) + @connection.receive_stderr(data) + end + end +end + + +module Ygg +module Provisioner +module Internals + +module Tools + def switch_files(f1, f2) + begin ; File.unlink(f2 + '.old') rescue Errno::ENOENT ; end + begin ; File.link(f2, f2 + '.old') rescue Errno::ENOENT ; end + File.rename(f1, f2) + end + + class CommandError < StandardError + attr_accessor :status + + def initialize(msg, status) + super msg + self.status = status + end + end + + class CommandRunner < EventMachine::Connection + extend Publisher + + can_fire :success, :failure, :receive_data, :receive_stderr + + # @private + def initialize(collect_data = true) + super + @out = '' + @collect_data = collect_data + end + + def self.open(cmd, collect_data = true) + EventMachine.popen3(cmd, self, collect_data) + end + + def receive_data(data) + @out += data if @collect_data + fire :receive_data, data + end + + def receive_stderr(data) + fire :receive_stderr, data + end + + def unbind + if get_status.success? + fire :success, @out, get_status + else + fire :failure, @out, get_status + end + end + end + + def run_command(cmd, opts = {}, &block) + if block + run_command_async(cmd, opts, &block) + + nil + else + fiber = Fiber.current + + run_command(cmd, opts) do |out, status| + fiber.resume(out, status) + end + + (out, status) = Fiber.yield + + if opts:with_status + out, status + else + raise CommandError.new('Error running command', status) if !status.success? + out + end + end + end + + #private + def run_command_async(cmd, opts = {}, &block) + out = '' + + plog = respond_to?(:plog) ? self.plog : opts:plog + + old_euid = Process::UID.eid + old_egid = Process::GID.eid + + if opts:uid + # This isn't the proper way to drop the privileges for the child process!!! FIXME TODO + Process::UID.grant_privilege(opts:uid) + end + + if opts:env + # This isn't the proper way to set the environment for the child process!!! FIXME TODO + opts:env.each { |k,v| ENVk = v } + end + + plog.debug " = running command #{cmd.inspect}" if plog + + c = CommandRunner.open(cmd, !opts:progress) + + if opts:send_data + c.send_data(opts:send_data) + end + + if opts:progress + c.on(:receive_data) do |out| + yield out, nil if block_given? + end + + c.on(:receive_stderr) do |out| + opts:stderr_handler.call(out) if opts:stderr_handler + end + end + + c.on(:success) do |out, result| + out.each_line { |l| plog.info l } if plog + + yield out, result if block_given? + end + + c.on(:failure) do |out, result| + out.each_line { |l| plog.error l } if plog + + yield out, result if block_given? + end + + Process::UID.eid = old_euid + Process::GID.eid = old_egid + + nil + end + +end + +end +end +end
View file
ygg_provisioner-1.4.1.gem/data/lib/ygg/provisioner/model.rb -> ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/model.rb
Changed
@@ -8,188 +8,247 @@ # License:: You can redistribute it and/or modify it under the terms of the LICENSE file. # -require 'kyotocabinet' -require 'ygg/provisioner/tools' -require 'ygg/provisioner/file_switcher' +require 'ostruct' +require 'active_support/core_ext' +require 'ygg/provisioner/internals/tools' +require 'ygg/provisioner/internals/file_switcher' +require 'ygg/provisioner/model_store' module Ygg module Provisioner +# +# == Ygg::Provisioner::Model +# +# Base class for provisioner models persisted in a database. +# It builds on top of +OpenStruct+ and provides: +# * a split between configuration (+cfg+) and runtime state (+state+) +# * persistence primitives backed by store +# * a small error hierarchy and database–checking utilities +# +# Subclasses are expected to: +# * set {.db_path} to the database path +# * configure {.state_persistant_keys} with the list of keys that belong +# to the local state +# * add whatever behaviour they need on top of the persistence layer +# +# A minimal example: +# class MyModel < Ygg::Provisioner::Model +# self.state_persistant_keys = :status +# end +# +# m = MyModel.new(:id => "my-id", :name => "example") +# m.update_state(:status => "pending") +# m.save! +# +# MyModel.find("my-id").status # => "pending" +# class Model < OpenStruct + # Generic superclass for all model‑related errors. class Error < StandardError ; end + + # Error raised when the data contained in the model are not valid + # from a domain point of view. class DataError < Error ; end + + # Error raised when an external process related to the model fails. class ProcessError < Error ; end + + # Error raised for database‑level problems (open, close, I/O errors). class DatabaseError < Error ; end - extend Tools - include Tools - include FileSwitcher + # Raised when an object cannot be found in the backing database. + class NotFound < Error ; end + + extend Internals::Tools + include Internals::Tools + include Internals::FileSwitcher + # Global agent configuration; copied into {#cfg} at initialization time. class_attribute :agentcfg + + # Logger used by agents; subclasses are free to configure it. class_attribute :log + + # Raw configuration for the agent running this model. class_attribute :config + + # Worker instance associated with this model, if any. class_attribute :worker + # List of keys that belong to the persistent *state* of the model. + # + # Keys listed here are written and read by {#update_state} and exported + # under +:state+ by {#serialize}. class_attribute :state_persistant_keys self.state_persistant_keys = + # Absolute path to the database backing this model. + # + # L'API rimane invariata rispetto alla versione precedente: i caller + # continuano a configurare {.db_path} per indicare dove si trova lo store. class_attribute :db_path - attr_accessor :plog - attr_accessor :config_persistant_keys - - def initialize(config) - - super(*) - - self.cfg = agentcfg - - if config:cfg - # We are being de-serialized - update_config(config:cfg) - update_state(config:state) - else - update_config(config) + # Store astratto utilizzato per persistere le istanze del model. + # + # Di default viene inizializzato in modo lazy a partire da {.db_path} + # tramite {.build_store_from_db_path}. Le sottoclassi possono + # ridefinire tale metodo per istanziare l'implementazione concreta + # di {Ygg::Provisioner::Store}. + class_attribute :store + + class << self + # Accessor per lo store: se non è stato impostato esplicitamente, + # viene costruito a partire da {.db_path}. + def store + @store = nil if @store&.database_configuration != db_path + @store ||= ModelStore.instantiate(db_path) end - end - - def update_config(config) - self.config_persistant_keys = config.keys.map(&:to_sym) - self.config_persistant_keys -= self.state_persistant_keys - config.each { |k,v| send("#{k}=", v) if self.config_persistant_keys.include?(k.to_sym) } - end - def update_state(state) - state.each { |k,v| send("#{k}=", v) if self.state_persistant_keys.include?(k.to_sym) } - end - - def save! - self.class.save(self) - end - - def serialize - { - :cfg => self.marshal_dump.select { |k,v| config_persistant_keys.include?(k.to_sym) }, - :state => self.marshal_dump.select { |k,v| state_persistant_keys.include?(k.to_sym) }, - :_type => self.class.name, - } - end - - def logging_on(plog) - @plog = plog - yield - end - - class NotFound < StandardError ; end - - @@open_store = false - - def self.with_open_store(&block) - if @@open_store - yield @@open_store - else - db = KyotoCabinet::DB::new - if db.open(db_path, KyotoCabinet::DB::OWRITER | KyotoCabinet::DB::OCREATE) - begin - @@open_store = db - yield db - ensure - unless db.close - raise DatabaseError.new("Cannot close database #{db_path}: #{db.error}") - end - @@open_store = nil - end - else - raise DatabaseError.new("Cannot open database #{db_path}: #{db.error}") - end + # Permette comunque di iniettare uno store custom (es. nei test). + def store=(value) + @store = value end - end - def self.save(model) - with_open_store do |store| - storemodel.id = Marshal.dump(model.serialize) + # Builds a new model instance from a serialized Hash. + # + # @param ser_obj Hash a Hash as produced by {#serialize}. + # + # If +ser_obj:_type+ is present, it is +constantize+‑d and used as + # the class to instantiate; otherwise the receiver (the class on which + # the method has been called) is used. + # + # @return Model a new instance initialised with the provided data. + def new_from_serialized(ser_obj) + klass = self + klass = ser_obj:_type.constantize if ser_obj:_type + obj = klass.new(ser_obj) + obj end - model - end - - def self.new_from_serialized(ser_obj) - klass = self - klass = ser_obj:_type.constantize if ser_obj:_type - obj = klass.new(ser_obj) - obj - end - - def self.each - with_open_store do |store| - store.each do |obj_id,obj| + # Iterates over all objects stored in the backing store. + # + # For each key/value pair nello store il valore viene passato + # opaco a {.new_from_serialized}, poi yield‑ato al chiamante. + # + # @yieldparam obj Model each deserialized model instance. + # @return void + # + # @note Corrupted records sono attualmente ignorati in silenzio; il blocco + # +rescue+ commentato mostra il comportamento di gestione errori + # desiderato. + def each + raise DatabaseError.new('Store not configured') unless store + + store.each_pair do |obj_id,obj| begin - obj = new_from_serialized(Marshal.load(obj)) - - yield obj + yield new_from_serialized(obj) -# rescue -# # Ignore corrupted obj data -# # plog.warn "Site #{obj_id} configuration is corrupted" +# rescue +# # Ignore corrupted obj data +# # plog.warn "Site #{obj_id} configuration is corrupted" end end end - end - def self.find(id) - obj = nil - with_open_store do |store| - obj = storeid + # Finds and returns a model instance by its identifier. + # + # @param id String identifier under which the object is stored. + # @return Model the deserialized model instance. + # @raise NotFound if no record exists for the given +id+. + # @raise DatabaseError on low‑level database errors. + def find(id) + raise DatabaseError.new('Store not configured') unless store + + obj = store.get(id) raise NotFound if !obj - obj = new_from_serialized(Marshal.load(obj)) + + new_from_serialized(obj) end - obj - end + # Serializes and persists the given model instance using the configured + # backing store. + # + # The model is stored under the key +model.id+ nello store configurato. + # Il valore è l'hash prodotto da {#serialize}, che viene gestito come + # opaco dallo store. + # + # @param model Model the instance to persist. + # @return Model the same instance passed in, for convenient chaining. + # @raise DatabaseError if the underlying store is not configured. + def save(model) + raise DatabaseError.new('Store not configured') unless store + + store.put(model.id, model.serialize) + + model + end + + # Deletes a model instance from the database. + # + # @param id String identifier of the record to delete. + # @return void + # @raise NotFound if no record exists for the given +id+. + # @raise DatabaseError on low‑level database errors. + def delete(id) + raise DatabaseError.new('Store not configured') unless store - def self.delete(id) - obj = nil - with_open_store do |store| - obj = storeid + obj = store.get(id) raise NotFound if !obj - store.delete id + + store.delete(id) end - end - def self.count - res = nil + # Returns the number of records stored in the backing database. + # + # @return Integer count of stored objects. + # @raise DatabaseError on low‑level database errors. + def count + raise DatabaseError.new('Store not configured') unless store - with_open_store do |store| - res = store.size + store.count end - res - end - - def self.fix_database(opts = {}) - corrupted_objs = {} - good_count = 0 - with_open_store do |store| - store.each do |obj_id,obj_marshalled| + # Scans the database and attempts to repair corrupted entries. + # + # For each record the method performs three steps: + # 1. +Marshal.load+ the raw value + # 2. {.new_from_serialized} to build a model instance + # 3. {#save!} on the rebuilt object + # + # Any failure during these phases is recorded in +:corrupted_objs+. + # + # @param opts Hash options hash. + # @option opts Boolean :delete_corrupted when +true+, corrupted + # records are removed from the database. + # + # @return Hash a report with: + # * +:good_count+ – number of objects successfully round‑tripped + # * +:corrupted_objs+ – Hash keyed by object id, containing details + # about failures and, optionally, backed‑up data + def fix_database(opts = {}) + corrupted_objs = {} + good_count = 0 + + store.each_pair do | obj_id, data, error | begin - obj_unmarshalled = nil - - begin - obj_unmarshalled = Marshal.load(obj_marshalled) - rescue Exception => e - corrupted_objsobj_id = { :reason => 'Unmarshal failed', :data => Base64.encode64(obj_marshalled) } + if !data && error + corrupted_objsobj_id = { + reason: 'Unmarshal failed', + data: Base64.encode64(error.raw_value) + } raise end obj = nil begin - obj = new_from_serialized(obj_unmarshalled) + obj = new_from_serialized(data) rescue Exception => e corrupted_objsobj_id = { - :reason => "Loading failed: #{e.to_s}", - :data => obj_unmarshalled + reason: "Loading failed: #{e.to_s}", + data: data } raise end @@ -199,8 +258,8 @@ good_count += 1 rescue Exception => e corrupted_objsobj_id = { - :reason => "Saving failed: #{e.to_s}", - :data => obj_unmarshalled + reason: "Saving failed: #{e.to_s}", + data: data } raise end @@ -211,12 +270,127 @@ end end end + + { + :good_count => good_count, + :corrupted_objs => corrupted_objs + } end + end + + # Per‑instance logger used by {#logging_on}. It is intentionally + # not persisted. + attr_accessor :plog + + # List of configuration keys that must be stored when serializing. + # + # This array is automatically populated by {#update_config} and used by + # {#serialize} to decide which attributes end up under +:cfg+. + attr_accessor :config_persistant_keys + + # Creates a new model instance. + # + # @param config Hash configuration data. + # + # Two calling patterns are supported: + # + # 1. A plain Hash of configuration attributes: + # + # ModelSubclass.new(:id => "id-1", :name => "foo") + # + # In this case the hash is passed directly to {#update_config}. + # + # 2. A Hash with +:cfg+ and +:state+ keys, as produced by {#serialize}: + # + # ModelSubclass.new(:cfg => {...}, :state => {...}) + # + # In this case +:cfg+ is fed to {#update_config} and +:state+ to + # {#update_state}. This is the path used during deserialization. + # + # In both cases, {#cfg} is initialised from {.agentcfg}. + def initialize(config = {}) - { - :good_count => good_count, - :corrupted_objs => corrupted_objs - } + super(*) + + self.cfg = agentcfg + + if config:cfg + # We are being de-serialized + update_config(config:cfg) + update_state(config:state) + else + update_config(config) + end + end + + # Updates configuration attributes from the given Hash. + # + # @param config Hash map of configuration keys and values. + # + # The method: + # * records all keys (as Symbols) into {#config_persistant_keys} + # * removes from that list any key that is present in + # {.state_persistant_keys}, to avoid clashes between config and state + # * assigns every remaining key on +self+ via +send("key=", value)+ + # + # Subsequent calls overwrite {#config_persistant_keys} with the keys of the + # *last* config hash, but they do not clear previous instance variables. + # Only the set of keys considered persistent for {#serialize} changes. + def update_config(config) + self.config_persistant_keys = config.keys.map(&:to_sym) + self.config_persistant_keys -= self.state_persistant_keys + config.each { |k,v| send("#{k}=", v) if self.config_persistant_keys.include?(k.to_sym) } + end + + # Updates state attributes from the given Hash. + # + # @param state Hash map of state keys and values. + # + # Only keys listed in {.state_persistant_keys} are applied. This prevents + # accidental overwrites of configuration attributes and keeps the state + # payload under control. + def update_state(state) + state.each { |k,v| send("#{k}=", v) if self.state_persistant_keys.include?(k.to_sym) } + end + + # Persists the model instance to the backing KyotoCabinet database. + # + # This is a convenience wrapper around {.save}. + # + # @return Model +self+, as returned by {.save}. + # @raise DatabaseError if the underlying database operation fails. + def save! + self.class.save(self) + end + + def delete! + self.class.delete(self.id) + end + + # Serializes the model into the Hash format stored in KyotoCabinet. + # + # @return Hash a Hash with the following keys: + # * +:cfg+ – configuration attributes, filtered by + # {#config_persistant_keys} + # * +:state+ – state attributes, filtered by {.state_persistant_keys} + # * +:_type+ – the concrete Ruby class name, used by + # {.new_from_serialized} to reconstruct the instance + def serialize + { + :cfg => self.marshal_dump.select { |k,v| config_persistant_keys.include?(k.to_sym) }, + :state => self.marshal_dump.select { |k,v| state_persistant_keys.include?(k.to_sym) }, + :_type => self.class.name, + } + end + + # Executes the given block with logging temporarily enabled. + # + # @param plog Object logger‑like object used within the block. + # @yield the block of work to execute with logging enabled. + # @return void + def logging_on(plog) + @plog = plog + yield end end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/model_store.rb
Added
@@ -0,0 +1,24 @@ +module Ygg + module Provisioner + module ModelStore + autoload :Sqlite3, 'ygg/provisioner/model_store/sqlite3' + autoload :Lmdb, 'ygg/provisioner/model_store/lmdb' + autoload :Kyotocabinet, 'ygg/provisioner/model_store/kyotocabinet' + + class << self + def instantiate(spec) + case spec + when spec.is_a?(String) && spec.end_with?('.kch') + Kyotocabinet.new(spec) + when spec.is_a?(String) && spec.end_with?('.sqlite3', '.sqlite', '.db') + Sqlite3.new(spec) + when spec.is_a?(String) && spec.end_with?('.lmdb', '.mdb') + Lmdb.new(spec) + else + raise ArgumentError, "Unsupported store specification: #{spec.inspect}" + end + end + end + end + end +end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/model_store/base.rb
Added
@@ -0,0 +1,115 @@ +module Ygg +module Provisioner +module ModelStore + + # + # == Ygg::Provisioner::Store + # + # Interfaccia astratta per gli store key/value utilizzati dai model. + # Implementazioni concrete (es. basate su KyotoCabinet, memoria, SQL, ecc.) + # devono specializzare questi metodi. + # + class Base + + # Wrapper per rappresentare un errore avvenuto durante + # l'unmarshal di un valore, mantenendo anche il payload grezzo. + class UnmarshalError < StandardError + attr_reader :raw_value, :error + + def initialize(error, raw_value) + super(error.message) + @raw_value = raw_value + @error = error + end + + def message + @error.message + end + + def backtrace + @error.backtrace + end + end + + attr_reader :database_configuration + + def initialize(database_configuration) + @database_configuration = database_configuration + end + + # Restituisce il valore associato a +key+ o +nil+ se assente. + # + # @param key String chiave logica dell'oggetto + # @return Object, nil + def get(key) + raise NotImplementedError + end + + # Imposta il valore associato a +key+. + # + # @param key String chiave logica dell'oggetto + # @param value Object payload serializzato da salvare + # @return void + def put(key, value) + raise NotImplementedError + end + + # Elimina la chiave indicata. + # + # @param key String chiave logica dell'oggetto + # @return Boolean true se la chiave esisteva ed è stata rimossa + def delete(key) + raise NotImplementedError + end + + # Itera su tutte le coppie chiave/valore presenti nello store. + # + # @yieldparam key String + # @yieldparam value Object + # @yieldparam err UnmarshalError, nil opzionale, solo se il blocco + # accetta tre argomenti: in caso di errore di unmarshal + # contiene l'oggetto di errore e il valore grezzo. + # @return void + def each_pair(&block) + return enum_for(:each_pair) unless block + + each_key_value do |key, raw| + if block.arity == 3 + begin + value = unmarshal(raw) + block.call(key, value, nil) + rescue => e + block.call(key, nil, UnmarshalError.new(e, raw)) + end + else + value = unmarshal(raw) + block.call(key, value) + end + end + end + + # Restituisce il numero totale di record presenti nello store. + # + # @return Integer + def count + raise NotImplementedError + end + + protected + + def marshal(data) + Marshal.dump(data) + end + + def unmarshal(data) + Marshal.load(data) + end + + def each_key_value(&block) + raise NotImplementedError + end + end + +end +end +end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/model_store/kyotocabinet.rb
Added
@@ -0,0 +1,89 @@ +require 'ygg/provisioner/model_store/base' +require 'kyotocabinet' + +module Ygg +module Provisioner +module ModelStore + + # Implementazione di store basata su KyotoCabinet. + # + # Usa un database key/value indicato da +db_path+ e implementa + # l'interfaccia {Ygg::Provisioner::ModelStore::Base}. + class Kyotocabinet < Base + # Restituisce il valore opaco associato a +key+ o +nil+ se assente. + def get(key) + with_db do |db| + raw = dbkey + raw && unmarshal(raw) + end + end + + # Imposta il valore opaco associato a +key+. + def put(key, value) + with_db do |db| + dbkey = marshal(value) + end + end + + # Elimina la chiave indicata, restituendo true se esisteva. + def delete(key) + with_db { |db| !!db.delete(key) } + end + + + # Restituisce il numero di record presenti nel database. + def count + with_db { |db| db.count } + end + + protected + + # Itera su tutte le coppie chiave/valore + def each_key_value(&block) + with_db do |db| + keys = + db.each_key do | k, _ | + keys << k + end + + keys.each do |key| + raw = dbkey + block.call(key, raw) + end + end + nil + end + + private + + # Apre il DB, esegue il blocco e lo chiude, sollevando DatabaseError + # in caso di problemi di open/close + def with_db + if @db_instance + yield @db_instance + else + @db_instance = KyotoCabinet::DB.new + + unless @db_instance.open(database_configuration, KyotoCabinet::DB::OWRITER | KyotoCabinet::DB::OCREATE) + raise Ygg::Provisioner::Model::DatabaseError.new( + "Cannot open database #{database_configuration}: #{@db_instance.error}" + ) + end + + begin + yield @db_instance + ensure + unless @db_instance.close + raise Ygg::Provisioner::Model::DatabaseError.new( + "Cannot close database #{database_configuration}: #{@db_instance.error}" + ) + end + @db_instance = nil + end + end + end + end + +end +end +end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/model_store/lmdb.rb
Added
@@ -0,0 +1,100 @@ +require 'ygg/provisioner/model_store/base' +require 'lmdb' +require 'fileutils' + +module Ygg +module Provisioner +module ModelStore + + # Implementazione di store basata su LMDB (Lightning Memory-Mapped Database). + # + # Usa un database LMDB indicato da +database_configuration+ e implementa + # l'interfaccia {Ygg::Provisioner::ModelStore::Base}, memorizzando + # un semplice key/value con valori marshal‑izzati. + class Lmdb < Base + def initialize(spec) + super + @env = nil + @db = nil + end + + # Restituisce il valore opaco associato a +key+ o +nil+ se assente. + def get(key) + with_db do |db| + raw = dbkey.to_s + raw && unmarshal(raw) + end + end + + # Imposta il valore opaco associato a +key+. + def put(key, value) + with_db do |db| + dbkey.to_s = marshal(value) + end + end + + # Elimina la chiave indicata, restituendo true se esisteva. + def delete(key) + with_db do |db| + existed = db.has?(key.to_s) + db.delete(key.to_s) if existed + existed + end + end + + # Restituisce il numero di record presenti nel database. + def count + with_db do |db| + db.stat:entries + end + end + + protected + + # Itera su tutte le coppie chiave/valore + def each_key_value(&block) + with_db do |db| + db.cursor do |cursor| + record = cursor.first + while record do + block.call(*record) + record = cursor.next + end + end + end + nil + end + + private + + # Apre il DB, esegue il blocco e lo chiude. + def with_db + if @db + @env.transaction do + yield @db + end + else + # Crea la directory se non esiste + db_path = database_configuration + FileUtils.mkdir_p(db_path) unless File.directory?(db_path) + + # Apri l'ambiente LMDB + @env = LMDB.new(db_path, maxdbs: 1) + @db = @env.database + + begin + @env.transaction do + yield @db + end + ensure + @env.close if @env + @db = nil + @env = nil + end + end + end + end + +end +end +end
View file
ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/model_store/sqlite3.rb
Added
@@ -0,0 +1,97 @@ +require 'ygg/provisioner/model_store/base' +require 'sqlite3' + +module Ygg +module Provisioner +module ModelStore + + # Implementazione di store basata su SQLite3. + # + # Usa un database SQLite indicato da +database_configuration+ e implementa + # l'interfaccia {Ygg::Provisioner::ModelStore::Base}, memorizzando + # un semplice key/value con valori marshal‑izzati. + class Sqlite3 < Base + def initialize(spec) + super + ensure_schema + end + + # Restituisce il valore opaco associato a +key+ o +nil+ se assente. + def get(key) + with_db do |db| + raw = db.get_first_value('SELECT value FROM kv_store WHERE key = ?', key) + raw && unmarshal(raw) + end + end + + # Imposta il valore opaco associato a +key+. + def put(key, value) + with_db do |db| + db.execute( + 'INSERT OR REPLACE INTO kv_store(key, value) VALUES(?, ?)', + key, + SQLite3::Blob.new(marshal(value)), + ) + end + end + + # Elimina la chiave indicata, restituendo true se esisteva. + def delete(key) + with_db do |db| + db.execute('DELETE FROM kv_store WHERE key = ?', key) + db.changes > 0 + end + end + + # Restituisce il numero di record presenti nel database. + def count + with_db do |db| + db.get_first_value('SELECT COUNT(*) FROM kv_store').to_i + end + end + + protected + + # Itera su tutte le coppie chiave/valore + def each_key_value(&block) + with_db do |db| + db.execute('SELECT key, value FROM kv_store') do |row| + key, raw = row + block.call(key, raw) + end + end + nil + end + + private + + def ensure_schema + with_db do |db| + db.execute <<-SQL + CREATE TABLE IF NOT EXISTS kv_store ( + key TEXT PRIMARY KEY, + value BLOB NOT NULL + ); + SQL + end + end + + # Apre il DB, esegue il blocco e lo chiude. + def with_db + if @db_instance + yield @db_instance + else + @db_instance = SQLite3::Database.new(database_configuration) + begin + yield @db_instance + ensure + @db_instance.close if @db_instance + @db_instance = nil + end + end + end + end + +end +end +end
View file
ygg_provisioner-1.4.1.gem/data/lib/ygg/provisioner/task.rb -> ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/task.rb
Changed
@@ -10,8 +10,8 @@ require 'fiber' -require 'ygg/provisioner/tools' -require 'ygg/provisioner/file_switcher' +require 'ygg/provisioner/internals/tools' +require 'ygg/provisioner/internals/file_switcher' module Ygg module Provisioner @@ -44,9 +44,9 @@ class InvalidRequestData < PermanentFailure ; end class OperationNotDefined < StandardError ; end - extend Tools - include Tools - include FileSwitcher + extend Internals::Tools + include Internals::Tools + include Internals::FileSwitcher class Operation attr_reader :help_caption
View file
ygg_provisioner-1.4.1.gem/data/lib/ygg/provisioner/version.rb -> ygg_provisioner-1.5.0.gem/data/lib/ygg/provisioner/version.rb
Changed
@@ -11,7 +11,7 @@ module Ygg module Provisioner - VERSION = '1.4.1' + VERSION = '1.5.0' end end
View file
ygg_provisioner-1.5.0.gem/data/spec/spec_helper.rb
Added
@@ -0,0 +1,100 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end
View file
ygg_provisioner-1.5.0.gem/data/spec/ygg/provisioner/model_spec.rb
Added
@@ -0,0 +1,517 @@ +require 'spec_helper' +require 'byebug' +require 'tmpdir' +require 'fileutils' +require 'ygg/provisioner/model' +require 'ygg/provisioner/model_store/base' + +RSpec.describe Ygg::Provisioner::Model do + class TestModel < described_class + self.state_persistant_keys = :status + end + + TEST_DB_DATA = { + 'obj-1' => { + :cfg => { :id => 'obj-1', :name => 'first', :value => 1 }, + :state => { :status => 'ok' }, + :_type => 'TestModel', + }, + 'obj-2' => { + :cfg => { :id => 'obj-2', :name => 'second', :value => 2 }, + :state => { :status => 'pending' }, + :_type => 'TestModel', + }, + }.freeze + + # Store in‑memory usato per tutti i test, per evitare dipendenze + # dallo store concreto (Kyoto, SQLite, ecc.). + class SimpleStore < Ygg::Provisioner::ModelStore::Base + def initialize(initial_records = {}) + # Copia profonda per non toccare TEST_DB_DATA + @records = {} + initial_records.each do | id, value | + put(id, value) + end + end + + def get(id) + unmarshal(@recordsid) if @recordsid + end + + def put(id, value) + @recordsid = marshal(value) + end + + def delete(id) + !!@records.delete(id) + end + + def count + @records.size + end + + def each_key_value(&block) + @records.each(&block) + end + + def raw_put(id, value) + @recordsid = value + end + end + + before(:each) do + store = SimpleStore.new(TEST_DB_DATA) + described_class.store = store + TestModel.store = store + end + + describe '#initialize' do + context 'when called with a plain config hash (no cfg/state wrapper)' do + it 'treats the hash as cfg and defines config accessors with values' do + subject = TestModel.new(name: 'plain', value: 42) + + expect(subject.config_persistant_keys).to contain_exactly(:name, :value) + expect(subject.name).to eq('plain') + expect(subject.value).to eq(42) + end + end + + context 'when called with cfg and state hashes' do + it 'uses cfg for config, state for state, and filters state by state_persistant_keys' do + config = { + cfg: { name: 'from_cfg', value: 10 }, + state: { status: 'from_state', other: 'ignored' }, + } + + subject = TestModel.new(config) + + expect(subject.config_persistant_keys).to contain_exactly(:name, :value) + expect(subject.name).to eq('from_cfg') + expect(subject.value).to eq(10) + + # Only keys in state_persistant_keys should be applied + expect(subject.status).to eq('from_state') + expect(subject.marshal_dump.keys).not_to include(:other) + end + end + + context 'when agentcfg is configured on the class' do + it 'sets cfg on the instance from agentcfg' do + begin + TestModel.agentcfg = { some: 'config' } + + subject = TestModel.new(name: 'with_agentcfg') + + expect(subject.cfg).to eq({ some: 'config' }) + ensure + TestModel.agentcfg = nil + end + end + end + end + + describe '#update_config' do + subject { TestModel.new({}) } + + context 'with a plain config hash' do + it 'sets config_persistant_keys and assigns config attributes' do + subject.update_config(name: 'foo', value: 1) + + expect(subject.config_persistant_keys).to contain_exactly(:name, :value) + expect(subject.name).to eq('foo') + expect(subject.value).to eq(1) + end + end + + context 'when config includes keys that are in state_persistant_keys' do + it 'does not treat state keys as config keys' do + subject.update_config(name: 'foo', status: 'ok') + + # status is in state_persistant_keys so it must not appear in config_persistant_keys + expect(subject.config_persistant_keys).to contain_exactly(:name) + expect(subject.name).to eq('foo') + + # update_config must not have written state keys + expect(subject.marshal_dump.keys).not_to include(:status) + end + end + + context 'when called multiple times' do + it 'recomputes config_persistant_keys from the last config hash' do + subject.update_config(name: 'foo', value: 1) + subject.update_config(name: 'bar') + + expect(subject.config_persistant_keys).to contain_exactly(:name) + expect(subject.name).to eq('bar') + + # previous attributes may still exist, but are no longer in config_persistant_keys + expect(subject.value).to eq(1) + expect(subject.config_persistant_keys).not_to include(:value) + end + end + end + + describe '#update_state' do + subject { TestModel.new({}) } + + context 'with a state hash including persistent and non-persistent keys' do + it 'applies only keys listed in state_persistant_keys' do + subject.update_state(status: 'ok', other: 'ignored') + + expect(subject.status).to eq('ok') + expect(subject.marshal_dump.keys).to include(:status) + expect(subject.marshal_dump.keys).not_to include(:other) + end + end + + context 'when config has already been set' do + it 'does not change config_persistant_keys or config values' do + subject.update_config(name: 'from_cfg') + + subject.update_state(status: 'ok') + + expect(subject.name).to eq('from_cfg') + expect(subject.config_persistant_keys).to contain_exactly(:name) + end + end + + context 'when updating the same state key multiple times' do + it 'overwrites the previous state value' do + subject.update_state(status: 'old') + subject.update_state(status: 'new') + + expect(subject.status).to eq('new') + expect(subject.marshal_dump:status).to eq('new') + end + end + + context 'when state keys clash with config keys' do + it 'does not allow state to overwrite config attributes' do + subject.update_config(name: 'from_cfg') + + subject.update_state(name: 'from_state', status: 'ok') + + # name must still be the one coming from config + expect(subject.name).to eq('from_cfg') + + # only status should be persisted as state + expect(subject.status).to eq('ok') + expect(subject.marshal_dump:name).to eq('from_cfg') + end + end + end + + describe '#save!' do + it 'persists a new record that can be found by id' do + subject = TestModel.new( + cfg: { id: 'new-1', name: 'new', value: 99 }, + state: { status: 'active' }, + ) + + subject.save! + + reloaded = TestModel.find('new-1') + expect(reloaded).to be_a(TestModel) + expect(reloaded.name).to eq('new') + expect(reloaded.value).to eq(99) + expect(reloaded.status).to eq('active') + end + + it 'increases the record count when saving a new id' do + + subject = TestModel.new( + cfg: { id: 'new-2', name: 'another', value: 1 }, + state: { status: 'pending' }, + ) + + expect { + subject.save! + }.to change{TestModel.count}.by(1) + + end + + it 'overwrites an existing record with the same id' do + subject = TestModel.new( + cfg: { id: 'dup-1', name: 'original', value: 1 }, + state: { status: 'old' }, + ) + + subject.save! + + subject.name = 'updated' + subject.update_state(status: 'new') + + expect { + subject.save! + }.not_to change{TestModel.count} + + reloaded = TestModel.find('dup-1') + expect(reloaded.name).to eq('updated') + expect(reloaded.value).to eq(1) + expect(reloaded.status).to eq('new') + end + end + + describe '#serialize' do + subject { TestModel.new({}) } + + context 'with config and state set' do + it 'separates cfg and state according to persistent keys' do + subject.update_config(name: 'foo', value: 1, temp: 'ignored_in_cfg') + subject.update_state(status: 'ok', other_state: 'ignored_in_state') + + # Restrict config_persistant_keys to a subset + subject.config_persistant_keys = :name, :value + + serialized = subject.serialize + + expect(serialized).to have_key(:cfg) + expect(serialized).to have_key(:state) + expect(serialized:cfg).to eq({ name: 'foo', value: 1 }) + expect(serialized:state).to eq({ status: 'ok' }) + end + end + + context 'with transient attributes' do + it 'does not include non-persistent attributes in cfg or state' do + subject.update_config(name: 'foo') + subject.update_state(status: 'ok') + subject.foo_transient = 'transient value' + + serialized = subject.serialize + + expect(serialized:cfg.keys).to contain_exactly(:name) + expect(serialized:state.keys).to contain_exactly(:status) + expect(serialized:cfg.values).not_to include('transient value') + expect(serialized:state.values).not_to include('transient value') + end + end + + context 'type information' do + it 'sets _type to the concrete model class name' do + serialized = subject.serialize + expect(serialized:_type).to eq('TestModel') + end + end + end + + describe '#logging_on' do + xit 'enables logging for the duration of the block' do + pending 'add expectations for logging_on behavior' + end + end + + describe '.save' do + it 'serializes and persists the given model, retrievable by id' do + model = TestModel.new( + cfg: { id: 'save-1', name: 'saved', value: 7 }, + state: { status: 'saving' }, + ) + + described_class.save(model) + + reloaded = TestModel.find('save-1') + expect(reloaded).to be_a(TestModel) + expect(reloaded.name).to eq('saved') + expect(reloaded.value).to eq(7) + expect(reloaded.status).to eq('saving') + end + + it 'returns the same model instance that was passed in' do + model = TestModel.new( + cfg: { id: 'save-2', name: 'returned', value: 3 }, + state: { status: 'ok' }, + ) + + returned = described_class.save(model) + expect(returned).to equal(model) + end + + it 'does not change the record count when saving an existing id' do + existing = TestModel.find('obj-1') + existing.name = 'modified' + existing.update_state(status: 'changed') + + expect { + described_class.save(existing) + }.not_to change{TestModel.count} + + reloaded = TestModel.find('obj-1') + expect(reloaded.name).to eq('modified') + expect(reloaded.status).to eq('changed') + end + end + + describe '.new_from_serialized' do + context 'when _type is not provided' do + it 'builds an instance of the receiver class using cfg and state' do + ser = { + cfg: { id: 'ns-1', name: 'no_type', value: 5 }, + state: { status: 'ok' }, + } + + obj = TestModel.new_from_serialized(ser) + + expect(obj).to be_a(TestModel) + expect(obj.id).to eq('ns-1') + expect(obj.name).to eq('no_type') + expect(obj.value).to eq(5) + expect(obj.status).to eq('ok') + end + end + + context 'when _type refers to TestModel' do + it 'constantizes _type and builds a TestModel instance' do + ser = { + cfg: { id: 'ns-2', name: 'typed', value: 10 }, + state: { status: 'typed-ok' }, + _type: 'TestModel', + } + + obj = described_class.new_from_serialized(ser) + + expect(obj).to be_a(TestModel) + expect(obj.id).to eq('ns-2') + expect(obj.name).to eq('typed') + expect(obj.value).to eq(10) + expect(obj.status).to eq('typed-ok') + end + end + + context 'when _type refers to another subclass' do + class AnotherModel < described_class + self.state_persistant_keys = :status + end + + it 'builds an instance of the subclass indicated by _type' do + ser = { + cfg: { id: 'ns-3', name: 'other', value: 20 }, + state: { status: 'other-ok' }, + _type: 'AnotherModel', + } + + obj = described_class.new_from_serialized(ser) + + expect(obj).to be_a(AnotherModel) + expect(obj.id).to eq('ns-3') + expect(obj.name).to eq('other') + expect(obj.value).to eq(20) + expect(obj.status).to eq('other-ok') + end + end + end + + describe '.each' do + it 'iterates over each stored object' do + collected = + + described_class.each do |obj| + collected << obj + end + + expect(collected.size).to eq(2) + names = collected.map(&:name) + values = collected.map(&:value) + statuses = collected.map(&:status) + + expect(names).to contain_exactly('first', 'second') + expect(values).to contain_exactly(1, 2) + expect(statuses).to contain_exactly('ok', 'pending') + end + end + + describe '.find' do + it 'returns the object for the given id' do + obj = described_class.find('obj-1') + + expect(obj).to be_a(TestModel) + expect(obj.name).to eq('first') + expect(obj.value).to eq(1) + expect(obj.status).to eq('ok') + end + + it 'raises NotFound for missing id' do + expect { + described_class.find('missing-id') + }.to raise_error(described_class::NotFound) + end + end + + describe '.delete' do + it 'deletes the object for the given id' do + # sanity check: the object exists + expect { described_class.find('obj-1') }.not_to raise_error + + described_class.delete('obj-1') + + expect { + described_class.find('obj-1') + }.to raise_error(described_class::NotFound) + end + + it 'raises NotFound when deleting a missing id' do + expect { + described_class.delete('missing-id') + }.to raise_error(described_class::NotFound) + end + end + + describe '.count' do + it 'returns the number of stored objects' do + expect(described_class.count).to eq(TEST_DB_DATA.size) + end + end + + describe '.fix_database' do + let(:corrupted) { 'this is not a valid marshalled data' } + let(:invalid) { 'this is not valid data' } + + before(:each) do + TestModel.store.raw_put('bad-unmarshal', corrupted) + TestModel.store.raw_put('bad-load', TestModel.store.send(:marshal, invalid)) + end + + it 'returns statistics about valid and corrupted objects' do + report = nil + + expect { + report = TestModel.fix_database + }.not_to change{TestModel.count} + + expect(report:good_count).to eq(2) + corrupted_objs = report:corrupted_objs + + expect(corrupted_objs.keys).to contain_exactly('bad-unmarshal', 'bad-load') + + expect(corrupted_objs'bad-unmarshal':reason).to eq('Unmarshal failed') + expect(corrupted_objs'bad-unmarshal':data).to eq(Base64.encode64(corrupted)) + + expect(corrupted_objs'bad-load':reason).to match(/Loading failed:/) + expect(corrupted_objs'bad-load':data).to eq(invalid) + end + + it 'deletes corrupted objects when delete_corrupted option is true' do + report = nil + + expect { + report = TestModel.fix_database(delete_corrupted: true) + }.to change{TestModel.count}.from(4).to(2) + + expect(report:corrupted_objs.keys).to contain_exactly('bad-unmarshal', 'bad-load') + expect(report:corrupted_objs'bad-unmarshal':deleted).to be true + expect(report:corrupted_objs'bad-load':deleted).to be true + end + + it 'does not delete corrupted objects when delete_corrupted option is false or missing' do + report = nil + + expect { + report = TestModel.fix_database + }.not_to change{TestModel.count} + + expect(report:corrupted_objs.keys).to contain_exactly('bad-unmarshal', 'bad-load') + expect(report:corrupted_objs'bad-unmarshal').not_to have_key(:deleted) + expect(report:corrupted_objs'bad-load').not_to have_key(:deleted) + end + end +end
View file
ygg_provisioner-1.5.0.gem/data/spec/ygg/provisioner/model_store/kyotocabinet_spec.rb
Added
@@ -0,0 +1,17 @@ +require 'spec_helper' +require 'tmpdir' +require 'fileutils' +require 'ygg/provisioner/model_store/kyotocabinet' +require 'ygg/provisioner/model_store_shared_examples' + +RSpec.describe Ygg::Provisioner::ModelStore::Kyotocabinet do + let(:tmp_dir) { Dir.mktmpdir('ygg_provisioner_store_spec') } + let(:db_path) { File.join(tmp_dir, 'store_test.kch') } + + after(:each) do + FileUtils.rm_rf(tmp_dir) if tmp_dir && Dir.exist?(tmp_dir) + end + + it_behaves_like 'a key-value model store', + Ygg::Provisioner::ModelStore::Kyotocabinet +end
View file
ygg_provisioner-1.5.0.gem/data/spec/ygg/provisioner/model_store/lmdb_spec.rb
Added
@@ -0,0 +1,17 @@ +require 'spec_helper' +require 'tmpdir' +require 'fileutils' +require 'ygg/provisioner/model_store/lmdb' +require 'ygg/provisioner/model_store_shared_examples' + +RSpec.describe Ygg::Provisioner::ModelStore::Lmdb do + let(:tmp_dir) { Dir.mktmpdir('ygg_provisioner_store_spec') } + let(:db_path) { File.join(tmp_dir, 'store_test.lmdb') } + + after(:each) do + FileUtils.rm_rf(tmp_dir) if tmp_dir && Dir.exist?(tmp_dir) + end + + it_behaves_like 'a key-value model store', + Ygg::Provisioner::ModelStore::Lmdb +end
View file
ygg_provisioner-1.5.0.gem/data/spec/ygg/provisioner/model_store/sqlite3_spec.rb
Added
@@ -0,0 +1,17 @@ +require 'spec_helper' +require 'tmpdir' +require 'fileutils' +require 'ygg/provisioner/model_store/sqlite3' +require 'ygg/provisioner/model_store_shared_examples' + +RSpec.describe Ygg::Provisioner::ModelStore::Sqlite3 do + let(:tmp_dir) { Dir.mktmpdir('ygg_provisioner_store_spec') } + let(:db_path) { File.join(tmp_dir, 'store_test.sqlite3') } + + after(:each) do + FileUtils.rm_rf(tmp_dir) if tmp_dir && Dir.exist?(tmp_dir) + end + + it_behaves_like 'a key-value model store', + Ygg::Provisioner::ModelStore::Sqlite3 +end
View file
ygg_provisioner-1.5.0.gem/data/spec/ygg/provisioner/model_store_shared_examples.rb
Added
@@ -0,0 +1,60 @@ +RSpec.shared_examples 'a key-value model store' do |store_class| + # Lo shared example assume che il contesto chiamante definisca: + # - let(:db_path) – specifica completa dello store + # + # In questo modo i singoli spec possono decidere come costruire lo + # store (file locale, path di rete, ecc.) senza che qui si facciano + # assunzioni su tmpdir o estensioni. + let(:store) { store_class.new(db_path) } + + it 'stores and retrieves opaque values with get/put' do + expect(store.get('missing')).to be_nil + + payload1 = { foo: 1 } + payload2 = 'a', 'b', 3 + + store.put('k1', payload1) + store.put('k2', payload2) + + expect(store.get('k1')).to eq(payload1) + expect(store.get('k2')).to eq(payload2) + end + + it 'deletes existing keys and returns proper boolean' do + store.put('k1', foo: 'bar') + + expect(store.delete('k1')).to be true + expect(store.get('k1')).to be_nil + + expect(store.delete('missing')).to be false + end + + it 'counts the number of stored records' do + expect(store.count).to eq(0) + + store.put('k1', 1) + store.put('k2', 2) + + expect(store.count).to eq(2) + + store.delete('k1') + expect(store.count).to eq(1) + end + + it 'iterates over all key/value pairs via each_pair' do + data = { + 'a' => { foo: 1 }, + 'b' => { bar: 2 }, + } + + data.each { |k, v| store.put(k, v) } + + seen = {} + + store.each_pair do |key, value| + seenkey = value + end + + expect(seen).to eq(data) + end +end
View file
ygg_provisioner-1.4.1.gem/data/ygg_provisioner.gemspec -> ygg_provisioner-1.5.0.gem/data/ygg_provisioner.gemspec
Changed
@@ -13,13 +13,17 @@ s.rubyforge_project = 'ygg_provisioner' - s.files = `git ls-files`.split("\n") + s.files = `git ls-files`.split("\n").reject{|f| f.start_with?('.') } s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = 'lib' s.add_runtime_dependency 'tomte-agents' - s.add_runtime_dependency 'kyotocabinet' s.add_runtime_dependency 'eventmachine', '~> 1.2.3' s.add_runtime_dependency 'publisher' + + s.add_development_dependency "rake" + s.add_development_dependency "pry" + s.add_development_dependency "rspec" + s.add_development_dependency "byebug" end
View file
ygg_provisioner-1.4.1.gem/metadata.gz -> ygg_provisioner-1.5.0.gem/metadata.gz
Changed
@@ -1,14 +1,14 @@ --- !ruby/object:Gem::Specification name: ygg_provisioner version: !ruby/object:Gem::Version - version: 1.4.1 + version: 1.5.0 platform: ruby authors: - Daniele Orlandi -autorequire: +autorequire: bindir: bin cert_chain: -date: 2023-09-12 00:00:00.000000000 Z +date: 2025-12-01 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: tomte-agents @@ -25,7 +25,21 @@ - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency - name: kyotocabinet + name: eventmachine + requirement: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: 1.2.3 + type: :runtime + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - "~>" + - !ruby/object:Gem::Version + version: 1.2.3 +- !ruby/object:Gem::Dependency + name: publisher requirement: !ruby/object:Gem::Requirement requirements: - - ">=" @@ -39,27 +53,55 @@ - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency - name: eventmachine + name: rake requirement: !ruby/object:Gem::Requirement requirements: - - - "~>" + - - ">=" - !ruby/object:Gem::Version - version: 1.2.3 - type: :runtime + version: '0' + type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - - "~>" + - - ">=" - !ruby/object:Gem::Version - version: 1.2.3 + version: '0' - !ruby/object:Gem::Dependency - name: publisher + name: pry requirement: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' - type: :runtime + type: :development + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' +- !ruby/object:Gem::Dependency + name: rspec + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + type: :development + prerelease: false + version_requirements: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' +- !ruby/object:Gem::Dependency + name: byebug + requirement: !ruby/object:Gem::Requirement + requirements: + - - ">=" + - !ruby/object:Gem::Version + version: '0' + type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: @@ -69,31 +111,43 @@ description: Provisioner base classes and helpers email: - daniele@orlandi.com -executables: +executables: +- ygg_provisioner_convert_store extensions: extra_rdoc_files: files: -- ".gitignore" -- ".gitlab-ci.yml" - Gemfile +- Gemfile.lock - README - Rakefile +- bin/ygg_provisioner_convert_store - lib/tomte/agents/skel_provisioner.rb - lib/ygg/provisioner.rb -- lib/ygg/provisioner/colon_delimited_file.rb -- lib/ygg/provisioner/file_switcher.rb +- lib/ygg/provisioner/internals/colon_delimited_file.rb +- lib/ygg/provisioner/internals/file_switcher.rb +- lib/ygg/provisioner/internals/tools.rb - lib/ygg/provisioner/model.rb +- lib/ygg/provisioner/model_store.rb +- lib/ygg/provisioner/model_store/base.rb +- lib/ygg/provisioner/model_store/kyotocabinet.rb +- lib/ygg/provisioner/model_store/lmdb.rb +- lib/ygg/provisioner/model_store/sqlite3.rb - lib/ygg/provisioner/system_db.rb - lib/ygg/provisioner/task.rb -- lib/ygg/provisioner/tools.rb - lib/ygg/provisioner/version.rb - lib/ygg/provisioner/worker.rb - lib/ygg_provisioner.rb +- spec/spec_helper.rb +- spec/ygg/provisioner/model_spec.rb +- spec/ygg/provisioner/model_store/kyotocabinet_spec.rb +- spec/ygg/provisioner/model_store/lmdb_spec.rb +- spec/ygg/provisioner/model_store/sqlite3_spec.rb +- spec/ygg/provisioner/model_store_shared_examples.rb - ygg_provisioner.gemspec homepage: http://www.yggdra.it/ licenses: metadata: {} -post_install_message: +post_install_message: rdoc_options: require_paths: - lib @@ -108,8 +162,8 @@ - !ruby/object:Gem::Version version: '0' requirements: -rubygems_version: 3.0.3 -signing_key: +rubygems_version: 3.5.22 +signing_key: specification_version: 4 summary: Provisioner base classes and helpers test_files:
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.