backup-bouchard 4.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (134) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +19 -0
  3. data/README.md +29 -0
  4. data/bin/backup +5 -0
  5. data/lib/backup.rb +140 -0
  6. data/lib/backup/archive.rb +169 -0
  7. data/lib/backup/binder.rb +18 -0
  8. data/lib/backup/cleaner.rb +112 -0
  9. data/lib/backup/cli.rb +370 -0
  10. data/lib/backup/cloud_io/base.rb +38 -0
  11. data/lib/backup/cloud_io/cloud_files.rb +296 -0
  12. data/lib/backup/cloud_io/s3.rb +253 -0
  13. data/lib/backup/compressor/base.rb +32 -0
  14. data/lib/backup/compressor/bzip2.rb +35 -0
  15. data/lib/backup/compressor/custom.rb +49 -0
  16. data/lib/backup/compressor/gzip.rb +73 -0
  17. data/lib/backup/config.rb +118 -0
  18. data/lib/backup/config/dsl.rb +100 -0
  19. data/lib/backup/config/helpers.rb +137 -0
  20. data/lib/backup/database/base.rb +86 -0
  21. data/lib/backup/database/mongodb.rb +187 -0
  22. data/lib/backup/database/mysql.rb +191 -0
  23. data/lib/backup/database/openldap.rb +93 -0
  24. data/lib/backup/database/postgresql.rb +132 -0
  25. data/lib/backup/database/redis.rb +177 -0
  26. data/lib/backup/database/riak.rb +79 -0
  27. data/lib/backup/database/sqlite.rb +55 -0
  28. data/lib/backup/encryptor/base.rb +27 -0
  29. data/lib/backup/encryptor/gpg.rb +740 -0
  30. data/lib/backup/encryptor/open_ssl.rb +74 -0
  31. data/lib/backup/errors.rb +53 -0
  32. data/lib/backup/logger.rb +197 -0
  33. data/lib/backup/logger/console.rb +48 -0
  34. data/lib/backup/logger/fog_adapter.rb +25 -0
  35. data/lib/backup/logger/logfile.rb +131 -0
  36. data/lib/backup/logger/syslog.rb +114 -0
  37. data/lib/backup/model.rb +477 -0
  38. data/lib/backup/notifier/base.rb +126 -0
  39. data/lib/backup/notifier/campfire.rb +61 -0
  40. data/lib/backup/notifier/command.rb +99 -0
  41. data/lib/backup/notifier/datadog.rb +104 -0
  42. data/lib/backup/notifier/flowdock.rb +99 -0
  43. data/lib/backup/notifier/hipchat.rb +116 -0
  44. data/lib/backup/notifier/http_post.rb +114 -0
  45. data/lib/backup/notifier/mail.rb +246 -0
  46. data/lib/backup/notifier/nagios.rb +65 -0
  47. data/lib/backup/notifier/pagerduty.rb +79 -0
  48. data/lib/backup/notifier/prowl.rb +68 -0
  49. data/lib/backup/notifier/pushover.rb +71 -0
  50. data/lib/backup/notifier/ses.rb +103 -0
  51. data/lib/backup/notifier/slack.rb +147 -0
  52. data/lib/backup/notifier/twitter.rb +55 -0
  53. data/lib/backup/notifier/zabbix.rb +60 -0
  54. data/lib/backup/package.rb +51 -0
  55. data/lib/backup/packager.rb +105 -0
  56. data/lib/backup/pipeline.rb +120 -0
  57. data/lib/backup/splitter.rb +73 -0
  58. data/lib/backup/storage/base.rb +66 -0
  59. data/lib/backup/storage/cloud_files.rb +156 -0
  60. data/lib/backup/storage/cycler.rb +70 -0
  61. data/lib/backup/storage/dropbox.rb +210 -0
  62. data/lib/backup/storage/ftp.rb +110 -0
  63. data/lib/backup/storage/local.rb +61 -0
  64. data/lib/backup/storage/qiniu.rb +65 -0
  65. data/lib/backup/storage/rsync.rb +246 -0
  66. data/lib/backup/storage/s3.rb +155 -0
  67. data/lib/backup/storage/scp.rb +65 -0
  68. data/lib/backup/storage/sftp.rb +80 -0
  69. data/lib/backup/syncer/base.rb +67 -0
  70. data/lib/backup/syncer/cloud/base.rb +176 -0
  71. data/lib/backup/syncer/cloud/cloud_files.rb +81 -0
  72. data/lib/backup/syncer/cloud/local_file.rb +97 -0
  73. data/lib/backup/syncer/cloud/s3.rb +109 -0
  74. data/lib/backup/syncer/rsync/base.rb +50 -0
  75. data/lib/backup/syncer/rsync/local.rb +27 -0
  76. data/lib/backup/syncer/rsync/pull.rb +47 -0
  77. data/lib/backup/syncer/rsync/push.rb +201 -0
  78. data/lib/backup/template.rb +41 -0
  79. data/lib/backup/utilities.rb +228 -0
  80. data/lib/backup/version.rb +3 -0
  81. data/templates/cli/archive +28 -0
  82. data/templates/cli/compressor/bzip2 +4 -0
  83. data/templates/cli/compressor/custom +7 -0
  84. data/templates/cli/compressor/gzip +4 -0
  85. data/templates/cli/config +123 -0
  86. data/templates/cli/databases/mongodb +15 -0
  87. data/templates/cli/databases/mysql +18 -0
  88. data/templates/cli/databases/openldap +24 -0
  89. data/templates/cli/databases/postgresql +16 -0
  90. data/templates/cli/databases/redis +16 -0
  91. data/templates/cli/databases/riak +17 -0
  92. data/templates/cli/databases/sqlite +11 -0
  93. data/templates/cli/encryptor/gpg +27 -0
  94. data/templates/cli/encryptor/openssl +9 -0
  95. data/templates/cli/model +26 -0
  96. data/templates/cli/notifier/zabbix +15 -0
  97. data/templates/cli/notifiers/campfire +12 -0
  98. data/templates/cli/notifiers/command +32 -0
  99. data/templates/cli/notifiers/datadog +57 -0
  100. data/templates/cli/notifiers/flowdock +16 -0
  101. data/templates/cli/notifiers/hipchat +16 -0
  102. data/templates/cli/notifiers/http_post +32 -0
  103. data/templates/cli/notifiers/mail +24 -0
  104. data/templates/cli/notifiers/nagios +13 -0
  105. data/templates/cli/notifiers/pagerduty +12 -0
  106. data/templates/cli/notifiers/prowl +11 -0
  107. data/templates/cli/notifiers/pushover +11 -0
  108. data/templates/cli/notifiers/ses +15 -0
  109. data/templates/cli/notifiers/slack +22 -0
  110. data/templates/cli/notifiers/twitter +13 -0
  111. data/templates/cli/splitter +7 -0
  112. data/templates/cli/storages/cloud_files +11 -0
  113. data/templates/cli/storages/dropbox +20 -0
  114. data/templates/cli/storages/ftp +13 -0
  115. data/templates/cli/storages/local +8 -0
  116. data/templates/cli/storages/qiniu +12 -0
  117. data/templates/cli/storages/rsync +17 -0
  118. data/templates/cli/storages/s3 +16 -0
  119. data/templates/cli/storages/scp +15 -0
  120. data/templates/cli/storages/sftp +15 -0
  121. data/templates/cli/syncers/cloud_files +22 -0
  122. data/templates/cli/syncers/rsync_local +20 -0
  123. data/templates/cli/syncers/rsync_pull +28 -0
  124. data/templates/cli/syncers/rsync_push +28 -0
  125. data/templates/cli/syncers/s3 +27 -0
  126. data/templates/general/links +3 -0
  127. data/templates/general/version.erb +2 -0
  128. data/templates/notifier/mail/failure.erb +16 -0
  129. data/templates/notifier/mail/success.erb +16 -0
  130. data/templates/notifier/mail/warning.erb +16 -0
  131. data/templates/storage/dropbox/authorization_url.erb +6 -0
  132. data/templates/storage/dropbox/authorized.erb +4 -0
  133. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  134. metadata +518 -0
@@ -0,0 +1,100 @@
1
+ module Backup
2
+ module Config
3
+ # Context for loading user config.rb and model files.
4
+ class DSL
5
+ class Error < Backup::Error; end
6
+ Model = Backup::Model
7
+
8
+ class << self
9
+ private
10
+
11
+ # List the available database, storage, syncer, compressor, encryptor
12
+ # and notifier constants. These are used to define constant names within
13
+ # Backup::Config::DSL so that users may use a constant instead of a string.
14
+ # Nested namespaces are represented using Hashs. Deep nesting supported.
15
+ #
16
+ # Example, instead of:
17
+ # database "MySQL" do |mysql|
18
+ # sync_with "RSync::Local" do |rsync|
19
+ #
20
+ # You can do:
21
+ # database MySQL do |mysql|
22
+ # sync_with RSync::Local do |rsync|
23
+ #
24
+ def add_dsl_constants
25
+ create_modules(
26
+ DSL,
27
+ [ # Databases
28
+ ["MySQL", "PostgreSQL", "MongoDB", "Redis", "Riak", "OpenLDAP", "SQLite"],
29
+ # Storages
30
+ ["S3", "CloudFiles", "Ninefold", "Dropbox", "FTP",
31
+ "SFTP", "SCP", "RSync", "Local", "Qiniu"],
32
+ # Compressors
33
+ ["Gzip", "Bzip2", "Custom", "Pbzip2", "Lzma"],
34
+ # Encryptors
35
+ ["OpenSSL", "GPG"],
36
+ # Syncers
37
+ [
38
+ { "Cloud" => ["CloudFiles", "S3"] },
39
+ { "RSync" => ["Push", "Pull", "Local"] }
40
+ ],
41
+ # Notifiers
42
+ ["Mail", "Twitter", "Campfire", "Prowl",
43
+ "Hipchat", "PagerDuty", "Pushover", "HttpPost", "Nagios",
44
+ "Slack", "FlowDock", "Zabbix", "Ses", "DataDog", "Command"]
45
+ ]
46
+ )
47
+ end
48
+
49
+ def create_modules(scope, names)
50
+ names.flatten.each do |name|
51
+ if name.is_a?(Hash)
52
+ name.each do |key, val|
53
+ create_modules(get_or_create_empty_module(scope, key), [val])
54
+ end
55
+ else
56
+ get_or_create_empty_module(scope, name)
57
+ end
58
+ end
59
+ end
60
+
61
+ def get_or_create_empty_module(scope, const)
62
+ if scope.const_defined?(const)
63
+ scope.const_get(const)
64
+ else
65
+ scope.const_set(const, Module.new)
66
+ end
67
+ end
68
+ end
69
+
70
+ add_dsl_constants # add constants on load
71
+
72
+ attr_reader :_config_options
73
+
74
+ def initialize
75
+ @_config_options = {}
76
+ end
77
+
78
+ # Allow users to set command line path options in config.rb
79
+ [:root_path, :data_path, :tmp_path].each do |name|
80
+ define_method name, ->(path) { _config_options[name] = path }
81
+ end
82
+
83
+ # Allows users to create preconfigured models.
84
+ def preconfigure(name, &block)
85
+ unless name.is_a?(String) && name =~ /^[A-Z]/
86
+ raise Error, "Preconfigured model names must be given as a string " \
87
+ "and start with a capital letter."
88
+ end
89
+
90
+ if DSL.const_defined?(name)
91
+ raise Error, "'#{name}' is already in use " \
92
+ "and can not be used for a preconfigured model."
93
+ end
94
+
95
+ DSL.const_set(name, Class.new(Model))
96
+ DSL.const_get(name).preconfigure(&block)
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,137 @@
1
+ require "ostruct"
2
+
3
+ module Backup
4
+ module Config
5
+ module Helpers
6
+ def self.included(klass)
7
+ klass.extend ClassMethods
8
+ end
9
+
10
+ module ClassMethods
11
+ def defaults
12
+ @defaults ||= Config::Defaults.new
13
+
14
+ if block_given?
15
+ yield @defaults
16
+ else
17
+ @defaults
18
+ end
19
+ end
20
+
21
+ # Used only within the specs
22
+ def clear_defaults!
23
+ defaults.reset!
24
+ end
25
+
26
+ def deprecations
27
+ @deprecations ||= {}
28
+ end
29
+
30
+ def log_deprecation_warning(name, deprecation)
31
+ msg = "#{self}##{name} has been deprecated as of " \
32
+ "backup v.#{deprecation[:version]}"
33
+ msg << "\n#{deprecation[:message]}" if deprecation[:message]
34
+ Logger.warn Config::Error.new(<<-EOS)
35
+ [DEPRECATION WARNING]
36
+ #{msg}
37
+ EOS
38
+ end
39
+
40
+ protected
41
+
42
+ ##
43
+ # Method to deprecate an attribute.
44
+ #
45
+ # :version
46
+ # Must be set to the backup version which will first
47
+ # introduce the deprecation.
48
+ #
49
+ # :action
50
+ # If set, this Proc will be called with a reference to the
51
+ # class instance and the value set on the deprecated accessor.
52
+ # e.g. deprecation[:action].call(klass, value)
53
+ # This should perform whatever action is neccessary, such as
54
+ # transferring the value to a new accessor.
55
+ #
56
+ # :message
57
+ # If set, this will be appended to #log_deprecation_warning
58
+ #
59
+ # Note that this replaces the `attr_accessor` method, or other
60
+ # method previously used to set the accessor being deprecated.
61
+ # #method_missing will handle any calls to `name=`.
62
+ #
63
+ def attr_deprecate(name, args = {})
64
+ deprecations[name] = {
65
+ version: nil,
66
+ message: nil,
67
+ action: nil
68
+ }.merge(args)
69
+ end
70
+ end # ClassMethods
71
+
72
+ private
73
+
74
+ ##
75
+ # Sets any pre-configured default values.
76
+ # If a default value was set for an invalid accessor,
77
+ # this will raise a NameError.
78
+ def load_defaults!
79
+ self.class.defaults._attributes.each do |name|
80
+ val = self.class.defaults.send(name)
81
+ val = val.dup rescue val
82
+ send(:"#{ name }=", val)
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Check missing methods for deprecated attribute accessors.
88
+ #
89
+ # If a value is set on an accessor that has been deprecated
90
+ # using #attr_deprecate, a warning will be issued and any
91
+ # :action (Proc) specified will be called with a reference to
92
+ # the class instance and the value set on the deprecated accessor.
93
+ # See #attr_deprecate and #log_deprecation_warning
94
+ #
95
+ # Note that OpenStruct (used for setting defaults) does not allow
96
+ # multiple arguments when assigning values for members.
97
+ # So, we won't allow it here either, even though an attr_accessor
98
+ # will accept and convert them into an Array. Therefore, setting
99
+ # an option value using multiple values, whether as a default or
100
+ # directly on the class' accessor, should not be supported.
101
+ # i.e. if an option will accept being set as an Array, then it
102
+ # should be explicitly set as such. e.g. option = [val1, val2]
103
+ #
104
+ def method_missing(name, *args)
105
+ deprecation = nil
106
+ if method = name.to_s.chomp!("=")
107
+ if (len = args.count) != 1
108
+ raise ArgumentError,
109
+ "wrong number of arguments (#{len} for 1)", caller(1)
110
+ end
111
+ deprecation = self.class.deprecations[method.to_sym]
112
+ end
113
+
114
+ if deprecation
115
+ self.class.log_deprecation_warning(method, deprecation)
116
+ deprecation[:action].call(self, args[0]) if deprecation[:action]
117
+ else
118
+ super
119
+ end
120
+ end
121
+ end # Helpers
122
+
123
+ # Store for pre-configured defaults.
124
+ class Defaults < OpenStruct
125
+ # Returns an Array of all attribute method names
126
+ # that default values were set for.
127
+ def _attributes
128
+ @table.keys
129
+ end
130
+
131
+ # Used only within the specs
132
+ def reset!
133
+ @table.clear
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,86 @@
1
+ module Backup
2
+ module Database
3
+ class Error < Backup::Error; end
4
+
5
+ class Base
6
+ include Utilities::Helpers
7
+ include Config::Helpers
8
+
9
+ attr_reader :model, :database_id, :dump_path
10
+
11
+ ##
12
+ # If given, +database_id+ will be appended to the #dump_filename.
13
+ # This is required if multiple Databases of the same class are added to
14
+ # the model.
15
+ def initialize(model, database_id = nil)
16
+ @model = model
17
+ @database_id = database_id.to_s.gsub(/\W/, "_") if database_id
18
+ @dump_path = File.join(Config.tmp_path, model.trigger, "databases")
19
+ load_defaults!
20
+ end
21
+
22
+ def perform!
23
+ log!(:started)
24
+ prepare!
25
+ end
26
+
27
+ private
28
+
29
+ def prepare!
30
+ FileUtils.mkdir_p(dump_path)
31
+ end
32
+
33
+ ##
34
+ # Sets the base filename for the final dump file to be saved in +dump_path+,
35
+ # based on the class name. e.g. databases/MySQL.sql
36
+ #
37
+ # +database_id+ will be appended if it is defined.
38
+ # e.g. databases/MySQL-database_id.sql
39
+ #
40
+ # If multiple Databases of the same class are defined and no +database_id+
41
+ # is defined, the user will be warned and one will be auto-generated.
42
+ #
43
+ # Model#initialize calls this method *after* all defined databases have
44
+ # been initialized so `backup check` can report these warnings.
45
+ def dump_filename
46
+ @dump_filename ||=
47
+ begin
48
+ unless database_id
49
+ if model.databases.select { |d| d.class == self.class }.count > 1
50
+ sleep 1
51
+ @database_id = Time.now.to_i.to_s[-5, 5]
52
+ Logger.warn Error.new(<<-EOS)
53
+ Database Identifier Missing
54
+ When multiple Databases are configured in a single Backup Model
55
+ that have the same class (MySQL, PostgreSQL, etc.), the optional
56
+ +database_id+ must be specified to uniquely identify each instance.
57
+ e.g. database MySQL, :database_id do |db|
58
+ This will result in an output file in your final backup package like:
59
+ databases/MySQL-database_id.sql
60
+
61
+ Backup has auto-generated an identifier (#{database_id}) for this
62
+ database dump and will now continue.
63
+ EOS
64
+ end
65
+ end
66
+
67
+ self.class.name.split("::").last + (database_id ? "-#{database_id}" : "")
68
+ end
69
+ end
70
+
71
+ def database_name
72
+ @database_name ||= self.class.to_s.sub("Backup::", "") +
73
+ (database_id ? " (#{database_id})" : "")
74
+ end
75
+
76
+ def log!(action)
77
+ msg =
78
+ case action
79
+ when :started then "Started..."
80
+ when :finished then "Finished!"
81
+ end
82
+ Logger.info "#{database_name} #{msg}"
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,187 @@
1
+ module Backup
2
+ module Database
3
+ class MongoDB < Base
4
+ class Error < Backup::Error; end
5
+
6
+ ##
7
+ # Name of the database that needs to get dumped
8
+ attr_accessor :name
9
+
10
+ ##
11
+ # Credentials for the specified database
12
+ attr_accessor :username, :password, :authdb
13
+
14
+ ##
15
+ # Connectivity options
16
+ attr_accessor :host, :port
17
+
18
+ ##
19
+ # IPv6 support (disabled by default)
20
+ attr_accessor :ipv6
21
+
22
+ ##
23
+ # Collections to dump, collections that aren't specified won't get dumped
24
+ attr_accessor :only_collections
25
+
26
+ ##
27
+ # Additional "mongodump" options
28
+ attr_accessor :additional_options
29
+
30
+ ##
31
+ # Forces mongod to flush all pending write operations to the disk and
32
+ # locks the entire mongod instance to prevent additional writes until the
33
+ # dump is complete.
34
+ #
35
+ # Note that if Profiling is enabled, this will disable it and will not
36
+ # re-enable it after the dump is complete.
37
+ attr_accessor :lock
38
+
39
+ ##
40
+ # Creates a dump of the database that includes an oplog, to create a
41
+ # point-in-time snapshot of the state of a mongod instance.
42
+ #
43
+ # If this option is used, you would not use the `lock` option.
44
+ #
45
+ # This will only work against nodes that maintain a oplog.
46
+ # This includes all members of a replica set, as well as master nodes in
47
+ # master/slave replication deployments.
48
+ attr_accessor :oplog
49
+
50
+ def initialize(model, database_id = nil, &block)
51
+ super
52
+ instance_eval(&block) if block_given?
53
+ end
54
+
55
+ def perform!
56
+ super
57
+
58
+ lock_database if @lock
59
+ dump!
60
+ package!
61
+
62
+ ensure
63
+ unlock_database if @lock
64
+ end
65
+
66
+ private
67
+
68
+ ##
69
+ # Performs all required mongodump commands, dumping the output files
70
+ # into the +dump_packaging_path+ directory for packaging.
71
+ def dump!
72
+ FileUtils.mkdir_p dump_packaging_path
73
+
74
+ collections = Array(only_collections)
75
+ if collections.empty?
76
+ run(mongodump)
77
+ else
78
+ collections.each do |collection|
79
+ run("#{mongodump} --collection='#{collection}'")
80
+ end
81
+ end
82
+ end
83
+
84
+ ##
85
+ # Creates a tar archive of the +dump_packaging_path+ directory
86
+ # and stores it in the +dump_path+ using +dump_filename+.
87
+ #
88
+ # <trigger>/databases/MongoDB[-<database_id>].tar[.gz]
89
+ #
90
+ # If successful, +dump_packaging_path+ is removed.
91
+ def package!
92
+ pipeline = Pipeline.new
93
+ dump_ext = "tar"
94
+
95
+ pipeline << "#{utility(:tar)} -cf - " \
96
+ "-C '#{dump_path}' '#{dump_filename}'"
97
+
98
+ if model.compressor
99
+ model.compressor.compress_with do |command, ext|
100
+ pipeline << command
101
+ dump_ext << ext
102
+ end
103
+ end
104
+
105
+ pipeline << "#{utility(:cat)} > " \
106
+ "'#{File.join(dump_path, dump_filename)}.#{dump_ext}'"
107
+
108
+ pipeline.run
109
+ if pipeline.success?
110
+ FileUtils.rm_rf dump_packaging_path
111
+ log!(:finished)
112
+ else
113
+ raise Error, "Dump Failed!\n#{pipeline.error_messages}"
114
+ end
115
+ end
116
+
117
+ def dump_packaging_path
118
+ File.join(dump_path, dump_filename)
119
+ end
120
+
121
+ def mongodump
122
+ "#{utility(:mongodump)} #{name_option} #{credential_options} " \
123
+ "#{connectivity_options} #{ipv6_option} #{oplog_option} " \
124
+ "#{user_options} --out='#{dump_packaging_path}'"
125
+ end
126
+
127
+ def name_option
128
+ return unless name
129
+ "--db='#{name}'"
130
+ end
131
+
132
+ def credential_options
133
+ opts = []
134
+ opts << "--username='#{username}'" if username
135
+ opts << "--password='#{password}'" if password
136
+ opts << "--authenticationDatabase='#{authdb}'" if authdb
137
+ opts.join(" ")
138
+ end
139
+
140
+ def connectivity_options
141
+ opts = []
142
+ opts << "--host='#{host}'" if host
143
+ opts << "--port='#{port}'" if port
144
+ opts.join(" ")
145
+ end
146
+
147
+ def ipv6_option
148
+ "--ipv6" if ipv6
149
+ end
150
+
151
+ def oplog_option
152
+ "--oplog" if oplog
153
+ end
154
+
155
+ def user_options
156
+ Array(additional_options).join(" ")
157
+ end
158
+
159
+ def lock_database
160
+ lock_command = <<-EOS.gsub(/^ +/, "")
161
+ echo 'use admin
162
+ db.setProfilingLevel(0)
163
+ db.fsyncLock()' | #{mongo_shell}
164
+ EOS
165
+
166
+ run(lock_command)
167
+ end
168
+
169
+ def unlock_database
170
+ unlock_command = <<-EOS.gsub(/^ +/, "")
171
+ echo 'use admin
172
+ db.fsyncUnlock()' | #{mongo_shell}
173
+ EOS
174
+
175
+ run(unlock_command)
176
+ end
177
+
178
+ def mongo_shell
179
+ cmd = "#{utility(:mongo)} #{connectivity_options}".rstrip
180
+ cmd << " #{credential_options}".rstrip
181
+ cmd << " #{ipv6_option}".rstrip
182
+ cmd << " '#{name}'" if name
183
+ cmd
184
+ end
185
+ end
186
+ end
187
+ end