cm-backup 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +20 -0
  3. data/bin/backup +5 -0
  4. data/lib/backup.rb +144 -0
  5. data/lib/backup/archive.rb +170 -0
  6. data/lib/backup/binder.rb +22 -0
  7. data/lib/backup/cleaner.rb +116 -0
  8. data/lib/backup/cli.rb +374 -0
  9. data/lib/backup/cloud_io/base.rb +41 -0
  10. data/lib/backup/cloud_io/cloud_files.rb +298 -0
  11. data/lib/backup/cloud_io/s3.rb +260 -0
  12. data/lib/backup/compressor/base.rb +35 -0
  13. data/lib/backup/compressor/bzip2.rb +39 -0
  14. data/lib/backup/compressor/custom.rb +53 -0
  15. data/lib/backup/compressor/gzip.rb +74 -0
  16. data/lib/backup/config.rb +119 -0
  17. data/lib/backup/config/dsl.rb +103 -0
  18. data/lib/backup/config/helpers.rb +143 -0
  19. data/lib/backup/database/base.rb +85 -0
  20. data/lib/backup/database/mongodb.rb +187 -0
  21. data/lib/backup/database/mysql.rb +192 -0
  22. data/lib/backup/database/openldap.rb +95 -0
  23. data/lib/backup/database/postgresql.rb +133 -0
  24. data/lib/backup/database/redis.rb +179 -0
  25. data/lib/backup/database/riak.rb +82 -0
  26. data/lib/backup/database/sqlite.rb +57 -0
  27. data/lib/backup/encryptor/base.rb +29 -0
  28. data/lib/backup/encryptor/gpg.rb +747 -0
  29. data/lib/backup/encryptor/open_ssl.rb +77 -0
  30. data/lib/backup/errors.rb +58 -0
  31. data/lib/backup/logger.rb +199 -0
  32. data/lib/backup/logger/console.rb +51 -0
  33. data/lib/backup/logger/fog_adapter.rb +29 -0
  34. data/lib/backup/logger/logfile.rb +133 -0
  35. data/lib/backup/logger/syslog.rb +116 -0
  36. data/lib/backup/model.rb +479 -0
  37. data/lib/backup/notifier/base.rb +128 -0
  38. data/lib/backup/notifier/campfire.rb +63 -0
  39. data/lib/backup/notifier/command.rb +102 -0
  40. data/lib/backup/notifier/datadog.rb +107 -0
  41. data/lib/backup/notifier/flowdock.rb +103 -0
  42. data/lib/backup/notifier/hipchat.rb +118 -0
  43. data/lib/backup/notifier/http_post.rb +117 -0
  44. data/lib/backup/notifier/mail.rb +249 -0
  45. data/lib/backup/notifier/nagios.rb +69 -0
  46. data/lib/backup/notifier/pagerduty.rb +81 -0
  47. data/lib/backup/notifier/prowl.rb +68 -0
  48. data/lib/backup/notifier/pushover.rb +74 -0
  49. data/lib/backup/notifier/ses.rb +105 -0
  50. data/lib/backup/notifier/slack.rb +148 -0
  51. data/lib/backup/notifier/twitter.rb +58 -0
  52. data/lib/backup/notifier/zabbix.rb +63 -0
  53. data/lib/backup/package.rb +55 -0
  54. data/lib/backup/packager.rb +107 -0
  55. data/lib/backup/pipeline.rb +124 -0
  56. data/lib/backup/splitter.rb +76 -0
  57. data/lib/backup/storage/base.rb +69 -0
  58. data/lib/backup/storage/cloud_files.rb +158 -0
  59. data/lib/backup/storage/cycler.rb +75 -0
  60. data/lib/backup/storage/dropbox.rb +212 -0
  61. data/lib/backup/storage/ftp.rb +112 -0
  62. data/lib/backup/storage/local.rb +64 -0
  63. data/lib/backup/storage/qiniu.rb +65 -0
  64. data/lib/backup/storage/rsync.rb +248 -0
  65. data/lib/backup/storage/s3.rb +156 -0
  66. data/lib/backup/storage/scp.rb +67 -0
  67. data/lib/backup/storage/sftp.rb +82 -0
  68. data/lib/backup/syncer/base.rb +70 -0
  69. data/lib/backup/syncer/cloud/base.rb +179 -0
  70. data/lib/backup/syncer/cloud/cloud_files.rb +83 -0
  71. data/lib/backup/syncer/cloud/local_file.rb +100 -0
  72. data/lib/backup/syncer/cloud/s3.rb +110 -0
  73. data/lib/backup/syncer/rsync/base.rb +54 -0
  74. data/lib/backup/syncer/rsync/local.rb +31 -0
  75. data/lib/backup/syncer/rsync/pull.rb +51 -0
  76. data/lib/backup/syncer/rsync/push.rb +205 -0
  77. data/lib/backup/template.rb +46 -0
  78. data/lib/backup/utilities.rb +224 -0
  79. data/lib/backup/version.rb +5 -0
  80. data/templates/cli/archive +28 -0
  81. data/templates/cli/compressor/bzip2 +4 -0
  82. data/templates/cli/compressor/custom +7 -0
  83. data/templates/cli/compressor/gzip +4 -0
  84. data/templates/cli/config +123 -0
  85. data/templates/cli/databases/mongodb +15 -0
  86. data/templates/cli/databases/mysql +18 -0
  87. data/templates/cli/databases/openldap +24 -0
  88. data/templates/cli/databases/postgresql +16 -0
  89. data/templates/cli/databases/redis +16 -0
  90. data/templates/cli/databases/riak +17 -0
  91. data/templates/cli/databases/sqlite +11 -0
  92. data/templates/cli/encryptor/gpg +27 -0
  93. data/templates/cli/encryptor/openssl +9 -0
  94. data/templates/cli/model +26 -0
  95. data/templates/cli/notifier/zabbix +15 -0
  96. data/templates/cli/notifiers/campfire +12 -0
  97. data/templates/cli/notifiers/command +32 -0
  98. data/templates/cli/notifiers/datadog +57 -0
  99. data/templates/cli/notifiers/flowdock +16 -0
  100. data/templates/cli/notifiers/hipchat +16 -0
  101. data/templates/cli/notifiers/http_post +32 -0
  102. data/templates/cli/notifiers/mail +24 -0
  103. data/templates/cli/notifiers/nagios +13 -0
  104. data/templates/cli/notifiers/pagerduty +12 -0
  105. data/templates/cli/notifiers/prowl +11 -0
  106. data/templates/cli/notifiers/pushover +11 -0
  107. data/templates/cli/notifiers/ses +15 -0
  108. data/templates/cli/notifiers/slack +22 -0
  109. data/templates/cli/notifiers/twitter +13 -0
  110. data/templates/cli/splitter +7 -0
  111. data/templates/cli/storages/cloud_files +11 -0
  112. data/templates/cli/storages/dropbox +20 -0
  113. data/templates/cli/storages/ftp +13 -0
  114. data/templates/cli/storages/local +8 -0
  115. data/templates/cli/storages/qiniu +12 -0
  116. data/templates/cli/storages/rsync +17 -0
  117. data/templates/cli/storages/s3 +16 -0
  118. data/templates/cli/storages/scp +15 -0
  119. data/templates/cli/storages/sftp +15 -0
  120. data/templates/cli/syncers/cloud_files +22 -0
  121. data/templates/cli/syncers/rsync_local +20 -0
  122. data/templates/cli/syncers/rsync_pull +28 -0
  123. data/templates/cli/syncers/rsync_push +28 -0
  124. data/templates/cli/syncers/s3 +27 -0
  125. data/templates/general/links +3 -0
  126. data/templates/general/version.erb +2 -0
  127. data/templates/notifier/mail/failure.erb +16 -0
  128. data/templates/notifier/mail/success.erb +16 -0
  129. data/templates/notifier/mail/warning.erb +16 -0
  130. data/templates/storage/dropbox/authorization_url.erb +6 -0
  131. data/templates/storage/dropbox/authorized.erb +4 -0
  132. data/templates/storage/dropbox/cache_file_written.erb +10 -0
  133. metadata +1077 -0
@@ -0,0 +1,69 @@
1
+ # encoding: utf-8
2
+
3
+ module Backup
4
+ module Notifier
5
+ class Nagios < Base
6
+
7
+ ##
8
+ # Host of Nagios server to notify on backup completion.
9
+ attr_accessor :nagios_host
10
+
11
+ ##
12
+ # Port of Nagios server to notify on backup completion.
13
+ attr_accessor :nagios_port
14
+
15
+ ##
16
+ # Nagios nrpe configuration file.
17
+ attr_accessor :send_nsca_cfg
18
+
19
+ ##
20
+ # Name of the Nagios service for the backup check.
21
+ attr_accessor :service_name
22
+
23
+ ##
24
+ # Host name in Nagios for the backup check.
25
+ attr_accessor :service_host
26
+
27
+ def initialize(model, &block)
28
+ super
29
+ instance_eval(&block) if block_given?
30
+
31
+ @nagios_host ||= Config.hostname
32
+ @nagios_port ||= 5667
33
+ @send_nsca_cfg||= "/etc/nagios/send_nsca.cfg"
34
+ @service_name ||= "Backup #{ model.trigger }"
35
+ @service_host ||= Config.hostname
36
+ end
37
+
38
+ private
39
+
40
+ ##
41
+ # Notify the user of the backup operation results.
42
+ #
43
+ # `status` indicates one of the following:
44
+ #
45
+ # `:success`
46
+ # : The backup completed successfully.
47
+ # : Notification will be sent if `on_success` is `true`.
48
+ #
49
+ # `:warning`
50
+ # : The backup completed successfully, but warnings were logged.
51
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
52
+ #
53
+ # `:failure`
54
+ # : The backup operation failed.
55
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
56
+ #
57
+ def notify!(status)
58
+ send_message(message.call(model, :status => status_data_for(status)))
59
+ end
60
+
61
+ def send_message(message)
62
+ cmd = "#{ utility(:send_nsca) } -H '#{ nagios_host }' -p '#{ nagios_port }' -c '#{ send_nsca_cfg }'"
63
+ msg = [service_host, service_name, model.exit_status, message].join("\t")
64
+ run("echo '#{ msg }' | #{ cmd }")
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,81 @@
1
+ # encoding: utf-8
2
+ require 'pagerduty'
3
+
4
+ module Backup
5
+ module Notifier
6
+ class PagerDuty < Base
7
+
8
+ ##
9
+ # PagerDuty Service API Key. Should be a 32 character hex string.
10
+ attr_accessor :service_key
11
+
12
+ ##
13
+ # Determines if a backup with a warning should resolve an incident rather
14
+ # than trigger one.
15
+ #
16
+ # Defaults to false.
17
+ attr_accessor :resolve_on_warning
18
+
19
+ def initialize(mode, &block)
20
+ super
21
+ instance_eval(&block) if block_given?
22
+
23
+ @resolve_on_warning ||= false
24
+ end
25
+
26
+ private
27
+
28
+ ##
29
+ # Trigger or resolve a PagerDuty incident for this model
30
+ #
31
+ # `status` indicates one of the following:
32
+ #
33
+ # `:success`
34
+ # : The backup completed successfully.
35
+ # : The incident will be resolved if `on_success` is `true`.
36
+ #
37
+ # `:warning`
38
+ # : The backup completed successfully, but warnings were logged.
39
+ # : An incident will be triggered if `on_warning` or `on_success` is `true`.
40
+ #
41
+ # `:failure`
42
+ # : The backup operation failed.
43
+ # : An incident will be triggered if `on_failure` is `true`.
44
+ #
45
+ def notify!(status)
46
+ incident_description = "Backup - #{model.label}"
47
+ incident_key = "backup/#{model.trigger}"
48
+ incident_details = {
49
+ :incident_key => incident_key,
50
+ :details => {
51
+ :trigger => model.trigger,
52
+ :label => model.label,
53
+ :started_at => model.started_at,
54
+ :finished_at => model.finished_at,
55
+ :duration => model.duration,
56
+ :status => status,
57
+ :exception => model.exception
58
+ }
59
+ }
60
+
61
+ event_type = case status
62
+ when :success then :resolve
63
+ when :warning then resolve_on_warning ? :resolve : :trigger
64
+ when :failure then :trigger
65
+ end
66
+
67
+ case event_type
68
+ when :trigger
69
+ pagerduty.trigger(incident_description, incident_details)
70
+ when :resolve
71
+ incident = pagerduty.get_incident(incident_key)
72
+ incident.resolve(incident_description, incident_details)
73
+ end
74
+ end
75
+
76
+ def pagerduty
77
+ @pagerduty ||= Pagerduty.new(service_key)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,68 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+
4
+ module Backup
5
+ module Notifier
6
+ class Prowl < Base
7
+
8
+ ##
9
+ # Application name
10
+ # Tell something like your server name. Example: "Server1 Backup"
11
+ attr_accessor :application
12
+
13
+ ##
14
+ # API-Key
15
+ # Create a Prowl account and request an API key on prowlapp.com.
16
+ attr_accessor :api_key
17
+
18
+ def initialize(model, &block)
19
+ @message = lambda do |model, data|
20
+ "#{ model.label } (#{ model.trigger })"
21
+ end
22
+ super
23
+ instance_eval(&block) if block_given?
24
+ end
25
+
26
+ private
27
+
28
+ ##
29
+ # Notify the user of the backup operation results.
30
+ #
31
+ # `status` indicates one of the following:
32
+ #
33
+ # `:success`
34
+ # : The backup completed successfully.
35
+ # : Notification will be sent if `on_success` is `true`.
36
+ #
37
+ # `:warning`
38
+ # : The backup completed successfully, but warnings were logged.
39
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
40
+ #
41
+ # `:failure`
42
+ # : The backup operation failed.
43
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
44
+ #
45
+ def notify!(status)
46
+ send_message(status)
47
+ end
48
+
49
+ def send_message(status)
50
+ uri = 'https://api.prowlapp.com/publicapi/add'
51
+ status_data = status_data_for(status)
52
+ data = {
53
+ :application => application,
54
+ :apikey => api_key,
55
+ :event => status_data[:message],
56
+ :description => message.call(model, :status => status_data)
57
+ }
58
+ options = {
59
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
60
+ :body => URI.encode_www_form(data)
61
+ }
62
+ options.merge!(:expects => 200) # raise error if unsuccessful
63
+ Excon.post(uri, options)
64
+ end
65
+
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,74 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+
4
+ module Backup
5
+ module Notifier
6
+ class Pushover < Base
7
+
8
+ ##
9
+ # The API User Token
10
+ attr_accessor :user
11
+
12
+ ##
13
+ # The API Application Token
14
+ attr_accessor :token
15
+
16
+ ##
17
+ # The user's device identifier to sent the message directly to,
18
+ # rather than all of the user's devices
19
+ attr_accessor :device
20
+
21
+ ##
22
+ # The message title
23
+ attr_accessor :title
24
+
25
+ ##
26
+ # The priority of the notification
27
+ attr_accessor :priority
28
+
29
+ def initialize(model, &block)
30
+ super
31
+ instance_eval(&block) if block_given?
32
+ end
33
+
34
+ private
35
+
36
+ ##
37
+ # Notify the user of the backup operation results.
38
+ #
39
+ # `status` indicates one of the following:
40
+ #
41
+ # `:success`
42
+ # : The backup completed successfully.
43
+ # : Notification will be sent if `on_success` is `true`.
44
+ #
45
+ # `:warning`
46
+ # : The backup completed successfully, but warnings were logged.
47
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
48
+ #
49
+ # `:failure`
50
+ # : The backup operation failed.
51
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
52
+ #
53
+ def notify!(status)
54
+ send_message(message.call(model, :status => status_data_for(status)))
55
+ end
56
+
57
+ def send_message(message)
58
+ uri = 'https://api.pushover.net/1/messages.json'
59
+ data = { :user => user, :token => token, :message => message }
60
+ [:device, :title, :priority].each do |param|
61
+ val = send(param)
62
+ data.merge!(param => val) if val
63
+ end
64
+ options = {
65
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
66
+ :body => URI.encode_www_form(data)
67
+ }
68
+ options.merge!(:expects => 200) # raise error if unsuccessful
69
+ Excon.post(uri, options)
70
+ end
71
+
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,105 @@
1
+ # encoding: utf-8
2
+ require 'aws/ses'
3
+
4
+ module Backup
5
+ module Notifier
6
+ class Ses < Base
7
+
8
+ ##
9
+ # Amazon Simple Email Service (SES) Credentials
10
+ attr_accessor :access_key_id, :secret_access_key
11
+
12
+ ##
13
+ # SES Region
14
+ attr_accessor :region
15
+
16
+ ##
17
+ # Sender Email Address
18
+ attr_accessor :from
19
+
20
+ ##
21
+ # Receiver Email Address
22
+ attr_accessor :to
23
+
24
+ ##
25
+ # CC receiver Email Address
26
+ attr_accessor :cc
27
+
28
+ ##
29
+ # BCC receiver Email Address
30
+ attr_accessor :bcc
31
+
32
+ ##
33
+ # Set reply to email address
34
+ attr_accessor :reply_to
35
+
36
+ def initialize(model, &block)
37
+ super
38
+ instance_eval(&block) if block_given?
39
+
40
+ @region ||= 'eu-west-1'
41
+ @send_log_on ||= [:warning, :failure]
42
+ end
43
+
44
+ ##
45
+ # Array of statuses for which the log file should be attached.
46
+ #
47
+ # Available statuses are: `:success`, `:warning` and `:failure`.
48
+ # Default: [:warning, :failure]
49
+ attr_accessor :send_log_on
50
+
51
+ private
52
+
53
+ def client
54
+ AWS::SES::Base.new(
55
+ :access_key_id => access_key_id,
56
+ :secret_access_key => secret_access_key,
57
+ :server => "email.#{region}.amazonaws.com"
58
+ )
59
+ end
60
+
61
+ ##
62
+ # Notify the user of the backup operation results.
63
+ #
64
+ # `status` indicates one of the following:
65
+ #
66
+ # `:success`
67
+ # : The backup completed successfully.
68
+ # : Notification will be sent if `on_success` is `true`.
69
+ #
70
+ # `:warning`
71
+ # : The backup completed successfully, but warnings were logged.
72
+ # : Notification will be sent, including a copy of the current
73
+ # : backup log, if `on_warning` or `on_success` is `true`.
74
+ #
75
+ # `:failure`
76
+ # : The backup operation failed.
77
+ # : Notification will be sent, including a copy of the current
78
+ # : backup log, if `on_failure` is `true`.
79
+ #
80
+ def notify!(status)
81
+ email = ::Mail.new
82
+ email.to = to
83
+ email.from = from
84
+ email.cc = cc
85
+ email.bcc = bcc
86
+ email.reply_to = reply_to
87
+ email.subject = message.call(model, :status => status_data_for(status))
88
+
89
+ send_log = send_log_on.include?(status)
90
+ template = Backup::Template.new({ :model => model, :send_log => send_log })
91
+ email.body = template.result('notifier/mail/%s.erb' % status.to_s)
92
+
93
+ if send_log
94
+ email.convert_to_multipart
95
+ email.attachments["#{ model.time }.#{ model.trigger }.log"] = {
96
+ :mime_type => 'text/plain;',
97
+ :content => Logger.messages.map(&:formatted_lines).flatten.join("\n")
98
+ }
99
+ end
100
+
101
+ client.send_raw_email(email)
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,148 @@
1
+ # encoding: utf-8
2
+ require 'uri'
3
+ require 'json'
4
+
5
+ module Backup
6
+ module Notifier
7
+ class Slack < Base
8
+
9
+ ##
10
+ # The incoming webhook url
11
+ attr_accessor :webhook_url
12
+
13
+ ##
14
+ # The channel to send messages to
15
+ attr_accessor :channel
16
+
17
+ ##
18
+ # The username to display along with the notification
19
+ attr_accessor :username
20
+
21
+ ##
22
+ # The emoji icon to display along with the notification
23
+ #
24
+ # See http://www.emoji-cheat-sheet.com for a list of icons.
25
+ #
26
+ # Default: :floppy_disk:
27
+ attr_accessor :icon_emoji
28
+
29
+ ##
30
+ # Array of statuses for which the log file should be attached.
31
+ #
32
+ # Available statuses are: `:success`, `:warning` and `:failure`.
33
+ # Default: [:warning, :failure]
34
+ attr_accessor :send_log_on
35
+
36
+ def initialize(model, &block)
37
+ super
38
+ instance_eval(&block) if block_given?
39
+
40
+ @send_log_on ||= [:warning, :failure]
41
+ @icon_emoji ||= ':floppy_disk:'
42
+ end
43
+
44
+ private
45
+
46
+ ##
47
+ # Notify the user of the backup operation results.
48
+ #
49
+ # `status` indicates one of the following:
50
+ #
51
+ # `:success`
52
+ # : The backup completed successfully.
53
+ # : Notification will be sent if `on_success` is `true`.
54
+ #
55
+ # `:warning`
56
+ # : The backup completed successfully, but warnings were logged.
57
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
58
+ #
59
+ # `:failure`
60
+ # : The backup operation failed.
61
+ # : Notification will be sent if `on_warning` or `on_success` is `true`.
62
+ #
63
+ def notify!(status)
64
+ data = {
65
+ :text => message.call(model, :status => status_data_for(status)),
66
+ :attachments => [attachment(status)]
67
+ }
68
+ [:channel, :username, :icon_emoji].each do |param|
69
+ val = send(param)
70
+ data.merge!(param => val) if val
71
+ end
72
+
73
+ options = {
74
+ :headers => { 'Content-Type' => 'application/x-www-form-urlencoded' },
75
+ :body => URI.encode_www_form(:payload => JSON.dump(data))
76
+ }
77
+ options.merge!(:expects => 200) # raise error if unsuccessful
78
+ Excon.post(uri, options)
79
+ end
80
+
81
+ def attachment(status)
82
+ {
83
+ :fallback => "#{title(status)} - Job: #{model.label} (#{model.trigger})",
84
+ :text => title(status),
85
+ :color => color(status),
86
+ :fields => [
87
+ {
88
+ :title => "Job",
89
+ :value => "#{model.label} (#{model.trigger})",
90
+ :short => false
91
+ },
92
+ {
93
+ :title => "Started",
94
+ :value => model.started_at,
95
+ :short => true
96
+ },
97
+ {
98
+ :title => "Finished",
99
+ :value => model.finished_at,
100
+ :short => true
101
+ },
102
+ {
103
+ :title => "Duration",
104
+ :value => model.duration,
105
+ :short => true
106
+ },
107
+ {
108
+ :title => "Version",
109
+ :value => "Backup v#{Backup::VERSION}\nRuby: #{RUBY_DESCRIPTION}",
110
+ :short => false
111
+ },
112
+ log_field(status)
113
+ ].compact
114
+ }
115
+ end
116
+
117
+ def log_field(status)
118
+ send_log = send_log_on.include?(status)
119
+
120
+ return {
121
+ :title => "Detailed Backup Log",
122
+ :value => Logger.messages.map(&:formatted_lines).flatten.join("\n"),
123
+ :short => false,
124
+ } if send_log
125
+ end
126
+
127
+ def color(status)
128
+ case status
129
+ when :success then 'good'
130
+ when :failure then 'danger'
131
+ when :warning then 'warning'
132
+ end
133
+ end
134
+
135
+ def title(status)
136
+ case status
137
+ when :success then 'Backup Completed Successfully!'
138
+ when :failure then 'Backup Failed!'
139
+ when :warning then 'Backup Completed Successfully (with Warnings)!'
140
+ end
141
+ end
142
+
143
+ def uri
144
+ @uri ||= webhook_url
145
+ end
146
+ end
147
+ end
148
+ end