backup 4.3.0 → 5.0.0.beta.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +19 -0
  3. data/README.md +13 -9
  4. data/bin/docker_test +24 -0
  5. data/lib/backup.rb +74 -78
  6. data/lib/backup/archive.rb +31 -32
  7. data/lib/backup/binder.rb +2 -6
  8. data/lib/backup/cleaner.rb +14 -18
  9. data/lib/backup/cli.rb +104 -108
  10. data/lib/backup/cloud_io/base.rb +4 -7
  11. data/lib/backup/cloud_io/cloud_files.rb +60 -62
  12. data/lib/backup/cloud_io/s3.rb +69 -76
  13. data/lib/backup/compressor/base.rb +4 -7
  14. data/lib/backup/compressor/bzip2.rb +3 -7
  15. data/lib/backup/compressor/custom.rb +2 -6
  16. data/lib/backup/compressor/gzip.rb +16 -17
  17. data/lib/backup/config.rb +17 -18
  18. data/lib/backup/config/dsl.rb +16 -17
  19. data/lib/backup/config/helpers.rb +10 -16
  20. data/lib/backup/database/base.rb +22 -21
  21. data/lib/backup/database/mongodb.rb +36 -37
  22. data/lib/backup/database/mysql.rb +40 -41
  23. data/lib/backup/database/openldap.rb +8 -10
  24. data/lib/backup/database/postgresql.rb +29 -30
  25. data/lib/backup/database/redis.rb +27 -30
  26. data/lib/backup/database/riak.rb +15 -18
  27. data/lib/backup/database/sqlite.rb +4 -6
  28. data/lib/backup/encryptor/base.rb +2 -4
  29. data/lib/backup/encryptor/gpg.rb +49 -59
  30. data/lib/backup/encryptor/open_ssl.rb +11 -14
  31. data/lib/backup/errors.rb +7 -12
  32. data/lib/backup/logger.rb +16 -18
  33. data/lib/backup/logger/console.rb +5 -8
  34. data/lib/backup/logger/fog_adapter.rb +2 -6
  35. data/lib/backup/logger/logfile.rb +10 -12
  36. data/lib/backup/logger/syslog.rb +2 -4
  37. data/lib/backup/model.rb +33 -40
  38. data/lib/backup/notifier/base.rb +24 -26
  39. data/lib/backup/notifier/campfire.rb +9 -11
  40. data/lib/backup/notifier/command.rb +3 -3
  41. data/lib/backup/notifier/datadog.rb +9 -12
  42. data/lib/backup/notifier/flowdock.rb +13 -17
  43. data/lib/backup/notifier/hipchat.rb +18 -14
  44. data/lib/backup/notifier/http_post.rb +11 -14
  45. data/lib/backup/notifier/mail.rb +42 -54
  46. data/lib/backup/notifier/nagios.rb +5 -9
  47. data/lib/backup/notifier/pagerduty.rb +10 -12
  48. data/lib/backup/notifier/prowl.rb +15 -15
  49. data/lib/backup/notifier/pushover.rb +7 -10
  50. data/lib/backup/notifier/ses.rb +52 -17
  51. data/lib/backup/notifier/slack.rb +39 -40
  52. data/lib/backup/notifier/twitter.rb +2 -5
  53. data/lib/backup/notifier/zabbix.rb +11 -14
  54. data/lib/backup/package.rb +5 -9
  55. data/lib/backup/packager.rb +16 -17
  56. data/lib/backup/pipeline.rb +17 -21
  57. data/lib/backup/splitter.rb +8 -11
  58. data/lib/backup/storage/base.rb +5 -8
  59. data/lib/backup/storage/cloud_files.rb +21 -23
  60. data/lib/backup/storage/cycler.rb +10 -15
  61. data/lib/backup/storage/dropbox.rb +15 -21
  62. data/lib/backup/storage/ftp.rb +14 -10
  63. data/lib/backup/storage/local.rb +5 -8
  64. data/lib/backup/storage/qiniu.rb +8 -8
  65. data/lib/backup/storage/rsync.rb +24 -26
  66. data/lib/backup/storage/s3.rb +27 -28
  67. data/lib/backup/storage/scp.rb +10 -12
  68. data/lib/backup/storage/sftp.rb +10 -12
  69. data/lib/backup/syncer/base.rb +5 -8
  70. data/lib/backup/syncer/cloud/base.rb +27 -30
  71. data/lib/backup/syncer/cloud/cloud_files.rb +16 -18
  72. data/lib/backup/syncer/cloud/local_file.rb +5 -8
  73. data/lib/backup/syncer/cloud/s3.rb +23 -24
  74. data/lib/backup/syncer/rsync/base.rb +6 -10
  75. data/lib/backup/syncer/rsync/local.rb +1 -5
  76. data/lib/backup/syncer/rsync/pull.rb +6 -10
  77. data/lib/backup/syncer/rsync/push.rb +18 -22
  78. data/lib/backup/template.rb +9 -14
  79. data/lib/backup/utilities.rb +78 -69
  80. data/lib/backup/version.rb +1 -3
  81. metadata +107 -677
@@ -1,9 +1,6 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  module Notifier
5
3
  class Zabbix < Base
6
-
7
4
  attr_accessor :zabbix_host
8
5
 
9
6
  attr_accessor :zabbix_port
@@ -19,10 +16,10 @@ module Backup
19
16
  instance_eval(&block) if block_given?
20
17
 
21
18
  @zabbix_host ||= Config.hostname
22
- @zabbix_port ||= 10051
23
- @service_name ||= "Backup #{ model.trigger }"
19
+ @zabbix_port ||= 10_051
20
+ @service_name ||= "Backup #{model.trigger}"
24
21
  @service_host ||= Config.hostname
25
- @item_key ||= 'backup_status'
22
+ @item_key ||= "backup_status"
26
23
  end
27
24
 
28
25
  private
@@ -45,18 +42,18 @@ module Backup
45
42
  # : Notification will be sent if `on_warning` or `on_success` is `true`.
46
43
  #
47
44
  def notify!(status)
48
- send_message(message.call(model, :status => status_data_for(status)))
45
+ send_message(message.call(model, status: status_data_for(status)))
49
46
  end
50
47
 
51
48
  def send_message(message)
52
49
  msg = [service_host, service_name, model.exit_status, message].join("\t")
53
- cmd = "#{ utility(:zabbix_sender) }" +
54
- " -z '#{ zabbix_host }'" +
55
- " -p '#{ zabbix_port }'" +
56
- " -s #{ service_host }" +
57
- " -k #{ item_key }" +
58
- " -o '#{ msg }'"
59
- run("echo '#{ msg }' | #{ cmd }")
50
+ cmd = utility(:zabbix_sender).to_s +
51
+ " -z '#{zabbix_host}'" \
52
+ " -p '#{zabbix_port}'" \
53
+ " -s #{service_host}" \
54
+ " -k #{item_key}" \
55
+ " -o '#{msg}'"
56
+ run("echo '#{msg}' | #{cmd}")
60
57
  end
61
58
  end
62
59
  end
@@ -1,8 +1,5 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Package
5
-
6
3
  ##
7
4
  # The time when the backup initiated (in format: 2011.02.20.03.29.59)
8
5
  attr_accessor :time
@@ -29,8 +26,8 @@ module Backup
29
26
 
30
27
  def initialize(model)
31
28
  @trigger = model.trigger
32
- @extension = 'tar'
33
- @chunk_suffixes = Array.new
29
+ @extension = "tar"
30
+ @chunk_suffixes = []
34
31
  @no_cycle = false
35
32
  @version = VERSION
36
33
  end
@@ -39,17 +36,16 @@ module Backup
39
36
  if chunk_suffixes.empty?
40
37
  [basename]
41
38
  else
42
- chunk_suffixes.map {|suffix| "#{ basename }-#{ suffix }" }
39
+ chunk_suffixes.map { |suffix| "#{basename}-#{suffix}" }
43
40
  end
44
41
  end
45
42
 
46
43
  def basename
47
- "#{ trigger }.#{ extension }"
44
+ "#{trigger}.#{extension}"
48
45
  end
49
46
 
50
47
  def time_as_object
51
- Time.strptime(time, '%Y.%m.%d.%H.%M.%S')
48
+ Time.strptime(time, "%Y.%m.%d.%H.%M.%S")
52
49
  end
53
-
54
50
  end
55
51
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  module Packager
5
3
  class Error < Backup::Error; end
@@ -22,7 +20,7 @@ module Backup
22
20
  Logger.info "Packaging Complete!"
23
21
  else
24
22
  raise Error, "Failed to Create Backup Package\n" +
25
- @pipeline.error_messages
23
+ @pipeline.error_messages
26
24
  end
27
25
  end
28
26
 
@@ -43,8 +41,8 @@ module Backup
43
41
  # or the Splitter (if no Encryptor), or through `cat` into the final
44
42
  # output file if neither are configured.
45
43
  @pipeline.add(
46
- "#{ utility(:tar) } -cf - " +
47
- "-C '#{ Config.tmp_path }' '#{ @package.trigger }'",
44
+ "#{utility(:tar)} -cf - " \
45
+ "-C '#{Config.tmp_path}' '#{@package.trigger}'",
48
46
  tar_success_codes
49
47
  )
50
48
 
@@ -75,26 +73,27 @@ module Backup
75
73
  #
76
74
  # If no Splitter was configured, the final file output will be
77
75
  # piped through `cat` into the final output file.
78
- if @splitter
79
- stack << lambda do
80
- @splitter.split_with do |command|
81
- @pipeline << command
76
+ stack <<
77
+ if @splitter
78
+ lambda do
79
+ @splitter.split_with do |command|
80
+ @pipeline << command
81
+ stack.shift.call
82
+ end
83
+ end
84
+ else
85
+ lambda do
86
+ outfile = File.join(Config.tmp_path, @package.basename)
87
+ @pipeline << "#{utility(:cat)} > #{outfile}"
82
88
  stack.shift.call
83
89
  end
84
90
  end
85
- else
86
- stack << lambda do
87
- outfile = File.join(Config.tmp_path, @package.basename)
88
- @pipeline << "#{ utility(:cat) } > #{ outfile }"
89
- stack.shift.call
90
- end
91
- end
92
91
 
93
92
  ##
94
93
  # Last Proc to be called runs the Pipeline the procedure built.
95
94
  # Once complete, the call stack will unwind back through the
96
95
  # preceeding Procs in the stack (if any)
97
- stack << lambda { @pipeline.run }
96
+ stack << -> { @pipeline.run }
98
97
 
99
98
  stack.shift
100
99
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Pipeline
5
3
  class Error < Backup::Error; end
@@ -12,7 +10,7 @@ module Backup
12
10
  @commands = []
13
11
  @success_codes = []
14
12
  @errors = []
15
- @stderr = ''
13
+ @stderr = ""
16
14
  end
17
15
 
18
16
  ##
@@ -51,22 +49,21 @@ module Backup
51
49
  # Use `#success?` to determine if all commands in the pipeline succeeded.
52
50
  # If `#success?` returns `false`, use `#error_messages` to get an error report.
53
51
  def run
54
- Open4.popen4(pipeline) do |pid, stdin, stdout, stderr|
55
- pipestatus = stdout.read.gsub("\n", '').split(':').sort
52
+ Open4.popen4(pipeline) do |_pid, _stdin, stdout, stderr|
53
+ pipestatus = stdout.read.delete("\n").split(":").sort
56
54
  pipestatus.each do |status|
57
- index, exitstatus = status.split('|').map(&:to_i)
58
- unless @success_codes[index].include?(exitstatus)
59
- command = command_name(@commands[index])
60
- @errors << SystemCallError.new(
61
- "'#{ command }' returned exit code: #{ exitstatus }", exitstatus
62
- )
63
- end
55
+ index, exitstatus = status.split("|").map(&:to_i)
56
+ next if @success_codes[index].include?(exitstatus)
57
+ command = command_name(@commands[index])
58
+ @errors << SystemCallError.new(
59
+ "'#{command}' returned exit code: #{exitstatus}", exitstatus
60
+ )
64
61
  end
65
62
  @stderr = stderr.read.strip
66
63
  end
67
64
  Logger.warn(stderr_messages) if success? && stderr_messages
68
65
  rescue Exception => err
69
- raise Error.wrap(err, 'Pipeline failed to execute')
66
+ raise Error.wrap(err, "Pipeline failed to execute")
70
67
  end
71
68
 
72
69
  def success?
@@ -78,9 +75,9 @@ module Backup
78
75
  # from the commands in the pipeline (if any), along with the SystemCallError
79
76
  # (Errno) message for each command which had a non-zero exit status.
80
77
  def error_messages
81
- @error_messages ||= (stderr_messages || '') +
82
- "The following system errors were returned:\n" +
83
- @errors.map {|err| "#{ err.class }: #{ err.message }" }.join("\n")
78
+ @error_messages ||= (stderr_messages || "") +
79
+ "The following system errors were returned:\n" +
80
+ @errors.map { |err| "#{err.class}: #{err.message}" }.join("\n")
84
81
  end
85
82
 
86
83
  private
@@ -106,19 +103,18 @@ module Backup
106
103
  def pipeline
107
104
  parts = []
108
105
  @commands.each_with_index do |command, index|
109
- parts << %Q[{ #{ command } 2>&4 ; echo "#{ index }|$?:" >&3 ; }]
106
+ parts << %({ #{command} 2>&4 ; echo "#{index}|$?:" >&3 ; })
110
107
  end
111
- %Q[{ #{ parts.join(' | ') } } 3>&1 1>&2 4>&2]
108
+ %({ #{parts.join(" | ")} } 3>&1 1>&2 4>&2)
112
109
  end
113
110
 
114
111
  def stderr_messages
115
- @stderr_messages ||= @stderr.empty? ? false : <<-EOS.gsub(/^ +/, ' ')
112
+ @stderr_messages ||= @stderr.empty? ? false : <<-EOS.gsub(/^ +/, " ")
116
113
  Pipeline STDERR Messages:
117
114
  (Note: may be interleaved if multiple commands returned error messages)
118
115
 
119
- #{ @stderr }
116
+ #{@stderr}
120
117
  EOS
121
118
  end
122
-
123
119
  end
124
120
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  class Splitter
5
3
  include Utilities::Helpers
@@ -21,8 +19,8 @@ module Backup
21
19
  # Once the packaging procedure is complete, it will return and
22
20
  # @package.chunk_suffixes will be set based on the resulting files.
23
21
  def split_with
24
- Logger.info "Splitter configured with a chunk size of #{ chunk_size }MB " +
25
- "and suffix length of #{ suffix_length }."
22
+ Logger.info "Splitter configured with a chunk size of #{chunk_size}MB " \
23
+ "and suffix length of #{suffix_length}."
26
24
  yield split_command
27
25
  after_packaging
28
26
  end
@@ -34,8 +32,8 @@ module Backup
34
32
  # multiple files, based on @chunk_size and @suffix_length, using the full
35
33
  # path to the final @package.basename, plus a '-' separator as the `prefix`.
36
34
  def split_command
37
- "#{ utility(:split) } -a #{ suffix_length } -b #{ chunk_size }m - " +
38
- "'#{ File.join(Config.tmp_path, package.basename + '-') }'"
35
+ "#{utility(:split)} -a #{suffix_length} -b #{chunk_size}m - " \
36
+ "'#{File.join(Config.tmp_path, package.basename + "-")}'"
39
37
  end
40
38
 
41
39
  ##
@@ -47,10 +45,10 @@ module Backup
47
45
  # remove the suffix from the filename.
48
46
  def after_packaging
49
47
  suffixes = chunk_suffixes
50
- first_suffix = 'a' * suffix_length
48
+ first_suffix = "a" * suffix_length
51
49
  if suffixes == [first_suffix]
52
50
  FileUtils.mv(
53
- File.join(Config.tmp_path, "#{ package.basename }-#{ first_suffix }"),
51
+ File.join(Config.tmp_path, "#{package.basename}-#{first_suffix}"),
54
52
  File.join(Config.tmp_path, package.basename)
55
53
  )
56
54
  else
@@ -62,15 +60,14 @@ module Backup
62
60
  # Returns an array of suffixes for each chunk, in alphabetical order.
63
61
  # For example: [aa, ab, ac, ad, ae] or [aaa, aab, aac aad]
64
62
  def chunk_suffixes
65
- chunks.map {|chunk| File.extname(chunk).split('-').last }.sort
63
+ chunks.map { |chunk| File.extname(chunk).split("-").last }.sort
66
64
  end
67
65
 
68
66
  ##
69
67
  # Returns an array of full paths to the backup chunks.
70
68
  # Chunks are sorted in alphabetical order.
71
69
  def chunks
72
- Dir[File.join(Config.tmp_path, package.basename + '-*')].sort
70
+ Dir[File.join(Config.tmp_path, package.basename + "-*")].sort
73
71
  end
74
-
75
72
  end
76
73
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  module Storage
5
3
  class Base
@@ -34,19 +32,19 @@ module Backup
34
32
  def initialize(model, storage_id = nil, &block)
35
33
  @model = model
36
34
  @package = model.package
37
- @storage_id = storage_id.to_s.gsub(/\W/, '_') if storage_id
35
+ @storage_id = storage_id.to_s.gsub(/\W/, "_") if storage_id
38
36
 
39
37
  load_defaults!
40
38
  instance_eval(&block) if block_given?
41
39
  end
42
40
 
43
41
  def perform!
44
- Logger.info "#{ storage_name } Started..."
42
+ Logger.info "#{storage_name} Started..."
45
43
  transfer!
46
44
  if respond_to?(:cycle!, true) && (keep.to_i > 0 || keep.is_a?(Time))
47
45
  cycle!
48
46
  end
49
- Logger.info "#{ storage_name } Finished!"
47
+ Logger.info "#{storage_name} Finished!"
50
48
  end
51
49
 
52
50
  private
@@ -60,10 +58,9 @@ module Backup
60
58
  alias :remote_path_for :remote_path
61
59
 
62
60
  def storage_name
63
- @storage_name ||= self.class.to_s.sub('Backup::', '') +
64
- (storage_id ? " (#{ storage_id })" : '')
61
+ @storage_name ||= self.class.to_s.sub("Backup::", "") +
62
+ (storage_id ? " (#{storage_id})" : "")
65
63
  end
66
-
67
64
  end
68
65
  end
69
66
  end
@@ -1,5 +1,4 @@
1
- # encoding: utf-8
2
- require 'backup/cloud_io/cloud_files'
1
+ require "backup/cloud_io/cloud_files"
3
2
 
4
3
  module Backup
5
4
  module Storage
@@ -79,8 +78,8 @@ module Backup
79
78
  @max_retries ||= 10
80
79
  @retry_waitsec ||= 30
81
80
 
82
- @path ||= 'backups'
83
- path.sub!(/^\//, '')
81
+ @path ||= "backups"
82
+ path.sub!(/^\//, "")
84
83
 
85
84
  check_configuration
86
85
  end
@@ -89,18 +88,18 @@ module Backup
89
88
 
90
89
  def cloud_io
91
90
  @cloud_io ||= CloudIO::CloudFiles.new(
92
- :username => username,
93
- :api_key => api_key,
94
- :auth_url => auth_url,
95
- :region => region,
96
- :servicenet => servicenet,
97
- :container => container,
98
- :segments_container => segments_container,
99
- :segment_size => segment_size,
100
- :days_to_keep => days_to_keep,
101
- :max_retries => max_retries,
102
- :retry_waitsec => retry_waitsec,
103
- :fog_options => fog_options
91
+ username: username,
92
+ api_key: api_key,
93
+ auth_url: auth_url,
94
+ region: region,
95
+ servicenet: servicenet,
96
+ container: container,
97
+ segments_container: segments_container,
98
+ segment_size: segment_size,
99
+ days_to_keep: days_to_keep,
100
+ max_retries: max_retries,
101
+ retry_waitsec: retry_waitsec,
102
+ fog_options: fog_options
104
103
  )
105
104
  end
106
105
 
@@ -108,7 +107,7 @@ module Backup
108
107
  package.filenames.each do |filename|
109
108
  src = File.join(Config.tmp_path, filename)
110
109
  dest = File.join(remote_path, filename)
111
- Logger.info "Storing '#{ container }/#{ dest }'..."
110
+ Logger.info "Storing '#{container}/#{dest}'..."
112
111
  cloud_io.upload(src, dest)
113
112
  end
114
113
 
@@ -118,12 +117,12 @@ module Backup
118
117
  # Called by the Cycler.
119
118
  # Any error raised will be logged as a warning.
120
119
  def remove!(package)
121
- Logger.info "Removing backup package dated #{ package.time }..."
120
+ Logger.info "Removing backup package dated #{package.time}..."
122
121
 
123
122
  remote_path = remote_path_for(package)
124
123
  objects = cloud_io.objects(remote_path)
125
124
 
126
- raise Error, "Package at '#{ remote_path }' not found" if objects.empty?
125
+ raise Error, "Package at '#{remote_path}' not found" if objects.empty?
127
126
 
128
127
  slo_objects, objects = objects.partition(&:slo?)
129
128
  cloud_io.delete_slo(slo_objects)
@@ -131,10 +130,10 @@ module Backup
131
130
  end
132
131
 
133
132
  def check_configuration
134
- required = %w{ username api_key container }
135
- raise Error, <<-EOS if required.map {|name| send(name) }.any?(&:nil?)
133
+ required = %w[username api_key container]
134
+ raise Error, <<-EOS if required.map { |name| send(name) }.any?(&:nil?)
136
135
  Configuration Error
137
- #{ required.map {|name| "##{ name }"}.join(', ') } are all required
136
+ #{required.map { |name| "##{name}" }.join(", ")} are all required
138
137
  EOS
139
138
 
140
139
  raise Error, <<-EOS if segment_size > 0 && segments_container.to_s.empty?
@@ -152,7 +151,6 @@ module Backup
152
151
  #segment_size is too large (max 5120)
153
152
  EOS
154
153
  end
155
-
156
154
  end
157
155
  end
158
156
  end
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  module Backup
4
2
  module Storage
5
3
  module Cycler
@@ -11,7 +9,7 @@ module Backup
11
9
  # and will remove any old package file(s) when the storage limit
12
10
  # set by #keep is exceeded.
13
11
  def cycle!
14
- Logger.info 'Cycling Started...'
12
+ Logger.info "Cycling Started..."
15
13
 
16
14
  packages = yaml_load.unshift(package)
17
15
  cycled_packages = []
@@ -32,24 +30,22 @@ module Backup
32
30
  end
33
31
 
34
32
  def delete_package(package)
35
- begin
36
- remove!(package) unless package.no_cycle
37
- rescue => err
38
- Logger.warn Error.wrap(err, <<-EOS)
33
+ remove!(package) unless package.no_cycle
34
+ rescue => err
35
+ Logger.warn Error.wrap(err, <<-EOS)
39
36
  There was a problem removing the following package:
40
37
  Trigger: #{package.trigger} :: Dated: #{package.time}
41
- Package included the following #{ package.filenames.count } file(s):
42
- #{ package.filenames.join("\n") }
38
+ Package included the following #{package.filenames.count} file(s):
39
+ #{package.filenames.join("\n")}
43
40
  EOS
44
- end
45
41
  end
46
42
 
47
43
  # Returns path to the YAML data file.
48
44
  def yaml_file
49
45
  @yaml_file ||= begin
50
- filename = self.class.to_s.split('::').last
51
- filename << "-#{ storage_id }" if storage_id
52
- File.join(Config.data_path, package.trigger, "#{ filename }.yml")
46
+ filename = self.class.to_s.split("::").last
47
+ filename << "-#{storage_id}" if storage_id
48
+ File.join(Config.data_path, package.trigger, "#{filename}.yml")
53
49
  end
54
50
  end
55
51
 
@@ -65,11 +61,10 @@ module Backup
65
61
  # Stores the given package objects to the YAML data file.
66
62
  def yaml_save(packages)
67
63
  FileUtils.mkdir_p(File.dirname(yaml_file))
68
- File.open(yaml_file, 'w') do |file|
64
+ File.open(yaml_file, "w") do |file|
69
65
  file.write(packages.to_yaml)
70
66
  end
71
67
  end
72
-
73
68
  end
74
69
  end
75
70
  end