Flucti-flucti-cli 0.1.16

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 (65) hide show
  1. data/LICENSE +7 -0
  2. data/README.mdown +24 -0
  3. data/Rakefile +56 -0
  4. data/TODO.txt +19 -0
  5. data/bin/flucti +4 -0
  6. data/flucti-cli.gemspec +46 -0
  7. data/lib/flucti.rb +20 -0
  8. data/lib/flucti/api_access.rb +56 -0
  9. data/lib/flucti/cli.rb +107 -0
  10. data/lib/flucti/parameters.rb +37 -0
  11. data/lib/flucti/resources.rb +41 -0
  12. data/lib/flucti/resources/account.rb +7 -0
  13. data/lib/flucti/resources/app_type.rb +18 -0
  14. data/lib/flucti/resources/backend.rb +28 -0
  15. data/lib/flucti/resources/basic_resource.rb +16 -0
  16. data/lib/flucti/resources/container.rb +15 -0
  17. data/lib/flucti/resources/db_server.rb +38 -0
  18. data/lib/flucti/resources/domain.rb +8 -0
  19. data/lib/flucti/resources/general.rb +14 -0
  20. data/lib/flucti/resources/mail_client.rb +7 -0
  21. data/lib/flucti/resources/mail_server.rb +7 -0
  22. data/lib/flucti/resources/port_forwarding.rb +15 -0
  23. data/lib/flucti/resources/port_forwarding/services +13921 -0
  24. data/lib/flucti/resources/ssh_details.rb +96 -0
  25. data/lib/flucti/resources/webserver.rb +9 -0
  26. data/lib/flucti/resources/website.rb +9 -0
  27. data/lib/flucti/tasks.rb +3 -0
  28. data/lib/flucti/tasks/apikey_tasks.rb +57 -0
  29. data/lib/flucti/tasks/apptype_tasks.rb +264 -0
  30. data/lib/flucti/tasks/connect_pack.rb +161 -0
  31. data/lib/flucti/tasks/db_tasks.rb +158 -0
  32. data/lib/flucti/tasks/mail_tasks.rb +104 -0
  33. data/lib/flucti/tasks/miscellaneous_tasks.rb +6 -0
  34. data/lib/flucti/tasks/progress_tasks.rb +24 -0
  35. data/lib/flucti/tasks/sshkey_tasks.rb +151 -0
  36. data/lib/flucti/tasks/vps/firewall_tasks.rb +84 -0
  37. data/lib/flucti/tasks/vps_tasks.rb +37 -0
  38. data/lib/flucti/tasks/webserver_tasks.rb +154 -0
  39. data/lib/flucti/tasks/website/apptype_tasks.rb +42 -0
  40. data/lib/flucti/tasks/website/backends/instances_tasks.rb +37 -0
  41. data/lib/flucti/tasks/website/backends_tasks.rb +254 -0
  42. data/lib/flucti/tasks/website/capfile_tasks.rb +41 -0
  43. data/lib/flucti/tasks/website/domains_tasks.rb +107 -0
  44. data/lib/flucti/tasks/website_tasks.rb +221 -0
  45. data/lib/flucti/utilities.rb +20 -0
  46. data/lib/flucti/utilities/connection_error_handling.rb +50 -0
  47. data/lib/flucti/utilities/core_ext.rb +35 -0
  48. data/lib/flucti/utilities/list_displayer.rb +57 -0
  49. data/lib/flucti/utilities/miscellaneous.rb +65 -0
  50. data/lib/flucti/utilities/progress_bar.rb +17 -0
  51. data/lib/flucti/utilities/table.rb +25 -0
  52. data/lib/flucti/utilities/task_packing.rb +10 -0
  53. data/lib/flucti/utilities/user_interface.rb +117 -0
  54. data/lib/vendor/ruby-progressbar-0.9/lib/ChangeLog +113 -0
  55. data/lib/vendor/ruby-progressbar-0.9/lib/progressbar.en.rd +103 -0
  56. data/lib/vendor/ruby-progressbar-0.9/lib/progressbar.ja.rd +100 -0
  57. data/lib/vendor/ruby-progressbar-0.9/lib/progressbar.rb +236 -0
  58. data/lib/vendor/ruby-progressbar-0.9/lib/test.rb +105 -0
  59. data/test/flucti/resources_test.rb +32 -0
  60. data/test/flucti/tasks_test.rb +28 -0
  61. data/test/flucti/utilities/miscellaneous_test.rb +54 -0
  62. data/test/flucti/utilities/table_test.rb +28 -0
  63. data/test/flucti/utilities/user_interface_test.rb +161 -0
  64. data/test/test_helper.rb +5 -0
  65. metadata +221 -0
@@ -0,0 +1,41 @@
1
+ namespace :website do
2
+ namespace :capfile do
3
+ DESTINATION = "Capfile"
4
+
5
+ task(:default) { show }
6
+
7
+ desc <<-DESC
8
+ Display the content of latest Capfile without downloading it.
9
+ DESC
10
+ task :show do
11
+ site = website.fetch_current
12
+ puts_title "Capfile for site: #{site}"
13
+ puts site.get("/capfile.rb")
14
+ end
15
+
16
+ desc <<-DESC
17
+ Download the latest Capfile for the current website.
18
+ DESC
19
+ task :download do
20
+ if File.exist?(DESTINATION) && !File.read(DESTINATION).blank?
21
+ puts_title "Backing up previous Capfile"
22
+ backup = Dir.tmpdir + "/#{File.basename DESTINATION}.#{Time.now.to_f}"
23
+ FileUtils.mv(DESTINATION, backup)
24
+ puts "Backup location: #{backup}"
25
+ end
26
+ puts_title "Downloading latest Capfile"
27
+ begin
28
+ File.open(DESTINATION, "w") do |f|
29
+ f.puts website.fetch_current!.get("/capfile.rb")
30
+ end
31
+ puts "Downloaded: %.2f KB" % [File.size(DESTINATION) / 1024.0]
32
+ rescue WebService::ResourceNotFound
33
+ puts "(No Capfile for the application type of the website.)"
34
+ end
35
+ end
36
+
37
+ def downloaded?
38
+ File.file?(DESTINATION)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,107 @@
1
+ namespace :website do
2
+ namespace :domains do
3
+ desc <<-DESC
4
+ Attach a domain to the current website. This can be one of:
5
+
6
+ * A domain of your own: setup instructions will be displayed after
7
+ successful attachment. For example: "foobar.com".
8
+
9
+ * A free subdomain: choose one ending with ".flucti.com": immediately
10
+ available, setup required.
11
+ For example: "foobar.flucti.com".
12
+
13
+ * A subdomain of either a domain of your own or a free subdomain.
14
+ For example: "blog.foobar.com" or "blog.foobar.flucti.com".
15
+
16
+ * A wildcard subdomain.
17
+ For example: "*.foobar.com" or "*.foobar.flucti.com"
18
+
19
+ Environment variables:
20
+ $DOMAIN: the domain name.
21
+ DESC
22
+ task :attach do
23
+ site = website.fetch_current!
24
+ name = require_name!
25
+
26
+ domain = site.domains.build(:name => name)
27
+
28
+ examples = []
29
+ try_save domain do
30
+ puts_title("Request sent")
31
+ puts_long <<-INFO
32
+ * Domain #{q domain} has been scheduled for attachment to website
33
+ #{q site}.
34
+ INFO
35
+ if domain.attribute_set?(:cname_domain_name)
36
+ puts
37
+ puts_long <<-INFO
38
+ * For #{q domain} to point to your VPS's, you must go to your
39
+ registrar's control panel, clear any previous A or CNAME
40
+ records, and add a CNAME record to #{q domain.cname_domain_name}.
41
+ INFO
42
+ end
43
+ end
44
+ end
45
+
46
+ desc "Alias for `attach'."
47
+ task(:add) { attach }
48
+
49
+ desc <<-DESC
50
+ Detach a domain from the current website. By doing so, the website will
51
+ cease to be available at the specified domain.
52
+
53
+ Environment variables:
54
+ $DOMAIN: the domain name.
55
+ DESC
56
+ task :detach do
57
+ site = website.fetch_current!
58
+ name = require_name!
59
+
60
+ domain = site.domains.find(name)
61
+ domain.destroy
62
+
63
+ puts_title("Request sent")
64
+ puts "Domain #{q domain} has been scheduled for detachment from website #{q site}."
65
+ end
66
+
67
+ desc "Alias for `attach'."
68
+ task(:remove) { detach }
69
+
70
+ desc <<-DESC
71
+ List all domains that need a CNAME record. In order for the domains you
72
+ own to point to the VPS hosting a site, you need to declare a CNAME
73
+ record on the DNS server of the domains, most likely managed by the
74
+ registrar they have been bought from.
75
+
76
+ Please note that it can take up to 48 hours for the changes to
77
+ propagate.
78
+
79
+ Free subdomains don't need such a declaration since they are handled on
80
+ our side. That is why they are not listed. As a consequence, they are
81
+ functional immediately.
82
+ DESC
83
+ task :cnames do
84
+ site = website.fetch_current!
85
+
86
+ domains_with_cname = site.domains.select { |dom| dom.cname_domain_name }
87
+
88
+ if domains_with_cname.any?
89
+ puts_list(domains_with_cname, :id => false, :table => true) do |t|
90
+ t.col("Domain", :name)
91
+ t.col("CNAME", :cname_domain_name)
92
+ end
93
+ else
94
+ puts "No domain needs to be declared CNAME records."
95
+ end
96
+ end
97
+
98
+ def require_name!
99
+ ENV["DOMAIN"] or
100
+ error! <<-MSG
101
+ The domain name to deal with must be specified by setting the
102
+ $DOMAIN environment variable. To list all domains currently attached
103
+ to the current website, run #{qcommand "website"}.
104
+ MSG
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,221 @@
1
+ namespace :website do
2
+ task(:default) { status }
3
+
4
+ desc <<-DESC
5
+ List of all your declared websites.
6
+ DESC
7
+ task :list do
8
+ puts_title "Declared websites"
9
+ list_displayer.display(Website.all)
10
+ end
11
+
12
+ desc <<-DESC
13
+ Display the names of all the declared websites. As opposed to
14
+ #{qcommand 'website:list'}, only the names are displayed, one name per
15
+ line. This is useful for batch processing.
16
+ DESC
17
+ task :names do
18
+ Website.all.each do |site|
19
+ puts(site.name)
20
+ end
21
+ end
22
+
23
+ desc <<-DESC
24
+ Display the status of the current website. That is, whether it is declared
25
+ or not, and if it is, displays its details.
26
+ DESC
27
+ task :status do
28
+ site = fetch_current!
29
+ puts_title "Website in the current directory"
30
+ show(site, :include_status)
31
+ end
32
+
33
+ desc "Alias for task `status'."
34
+ task(:show) { status }
35
+
36
+ desc <<-DESC
37
+ Declare the current website for subsequent deploys. This is necessary for
38
+ the site to be made online. That is, for our routers to direct traffic to
39
+ and from your VPS's.
40
+
41
+ Environment variables:
42
+ (optional) $SITEID: the name of the site for it to be referred to as later.
43
+ Default: the name the current working directory.
44
+
45
+ (optional) $TYPE: the ID or short name of the corresponding application
46
+ type (#{qcommand "apptype:list"} to see what stock
47
+ app. types are available, or #{qcommand "apptype:push"}
48
+ to create your own).
49
+ DESC
50
+ task :declare do
51
+ if site = fetch_current
52
+ puts "Already declared (as #{q site})"
53
+ next
54
+ end
55
+
56
+ # Find the application type
57
+ type =
58
+ if short = ENV["TYPE"]
59
+ AppType.all.find { |t| t.short == short } or
60
+ error! <<-MSG
61
+ Application type #{q short} does not exist. For a list of
62
+ available application types, run #{qcommand "apptype:list"}.
63
+ MSG
64
+ else
65
+ autodetect_app_type
66
+ end
67
+
68
+ # Prepare site for declaration
69
+ site = Website.new :name => name_of_current,
70
+ :app_type => type
71
+
72
+ # Attempt to push the declaration to the server
73
+ try_save site do
74
+ puts_title "Site declared"
75
+ show(site)
76
+
77
+ puts_subtitle("What to do next")
78
+ puts_long <<-INFO
79
+ 1) Database connection:
80
+ -----------------------
81
+ If your site is database backed, and if you haven't done so already,
82
+ you should now set up a database server with #{qcommand \
83
+ "db:server:setup"} and modify the production database connection
84
+ settings accordingly in the configuration files (for example:
85
+ wp-config.php for a WordPress blog, config/database.yml for a Rails
86
+ application).
87
+
88
+ 2) Backends:
89
+ ------------
90
+ For your site to be online, webservers must receive requests from
91
+ visitors and turn to backend processes (typically PHP or Rails) to
92
+ process them. So there has to exist at least one webserver running,
93
+ and at least one backend on one of the webserver VPS's. Such an
94
+ architecture can be put in place by:
95
+
96
+ * Setting up an initial, minimal stack, in an automated fashion:
97
+ $ #{command "website:ensure_deployable"}
98
+
99
+ * or going through each step manually for finer grained control.
100
+ Example:
101
+ $ #{command "webserver:setup ON=felix"}
102
+ $ #{command "website:backends:add ON=felix"}
103
+
104
+ The actions of whichever option you choose can be reverted or altered
105
+ later.
106
+
107
+ 3) Deployment:
108
+ --------------
109
+ To upload the files of your website to the backends, you can either:
110
+
111
+ * Download and run an automatically generated recipe, in the form of
112
+ a Capfile:
113
+ $ #{command "website:capfile:download"}
114
+ $ cap deploy:setup deploy
115
+
116
+ * Connect to each backend individually and upload the files to them.
117
+ For example:
118
+ $ #{command "website:backends:connect:details"}
119
+ $ scp -P 1025 -r * foo@bar.flucti.net:~/mysite/current
120
+
121
+ 5) Wait:
122
+ --------
123
+ Wait a few moments for all automated actions to finish, and then your
124
+ site should be online at http://#{site.domains.first}:
125
+ $ #{command "progress"}
126
+
127
+ 6) Domain:
128
+ ----------
129
+ The default domain that has been attached automatically to your site
130
+ can be replaced by attaching your actual domain with
131
+ #{qcommand("website:domains:attach")} and removing the default one
132
+ with #{qcommand("website:domains:detach")}.
133
+ INFO
134
+ end
135
+ end
136
+
137
+ desc <<-DESC
138
+ Make sure the site can be deployed. For this to be the case, there has to
139
+ exist at least one webserver: one is set up if none is yet. Also, the
140
+ website has to be running on at least one of the VPS that run a webserver.
141
+ Failing that, the VPS with the webserver last set up is chosen as the
142
+ first host for the website. Those two parameters can be altered later.
143
+ DESC
144
+ task :ensure_deployable do
145
+ website = fetch_current!
146
+
147
+ if (count = website.backends.all.size).zero?
148
+ webserver.ensure_any
149
+ backends.add
150
+ else
151
+ puts_title "Backends present"
152
+ puts "There exists #{count} backends."
153
+ end
154
+ end
155
+
156
+ desc "Alias for task `website:backends:enter'."
157
+ task(:enter) { backends.enter }
158
+
159
+ desc <<-DESC
160
+ Delete a website and all of its backends. This will take it offline and
161
+ soft-clear all files stored in the backends (see #{qcommand 'website:backends:remove'}).
162
+ DESC
163
+ task :delete do
164
+ site = fetch_current!
165
+ confirm <<-WARN
166
+ Deleting a website will make it will:
167
+
168
+ * Cancel its declaration, making it unavailable.
169
+
170
+ * Do the equivalent of #{qcommand "website:backends:remove"} for all the
171
+ backends, so be sure to read the documentation of this command
172
+ (#{qcommand '-e website:backends:remove'}) before confirming.
173
+ WARN
174
+ site.destroy
175
+ puts_title "Request sent"
176
+ puts "Website #{q site} scheduled for deletion."
177
+ end
178
+
179
+ def name_of_current
180
+ ENV['SITEID'] || clean_name(Pathname.pwd.basename)
181
+ end
182
+
183
+ def fetch_current
184
+ Website[name_of_current]
185
+ end
186
+
187
+ def fetch_current!
188
+ fetch_current or
189
+ error! <<-MSG
190
+ The current website is undeclared yet (or at least not as
191
+ #{q name_of_current}). Run #{qcommand "website:declare"} to declare it.
192
+ MSG
193
+ end
194
+
195
+ def autodetect_app_type
196
+ types = AppType.all.to_a
197
+ static = types.find { |t| t.short == 'static' }
198
+ (types - [static]).
199
+ sort_by { |t| %w(rails php).index(t.short) || -1 }.
200
+ each { |apptype|
201
+ if test = apptype.detection
202
+ filetype, path = test.split(' ', 2)
203
+ return apptype if Kernel.test(filetype, path)
204
+ end
205
+ }
206
+ static
207
+ end
208
+
209
+ def show(site, *args)
210
+ list_displayer(*args).display_single_entry(site)
211
+ end
212
+
213
+ def list_displayer(include_status=false)
214
+ Utilities::ListDisplayer.new(:title => :name) do |t|
215
+ t.col("Status") { "Declared" } if include_status
216
+ t.col("Name", :name)
217
+ t.col("Type", :app_type)
218
+ t.col("Domains") { |s| (domains = s.domains.to_a).any? ? domains * ', ' : '(none)' }
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,20 @@
1
+ module Flucti
2
+ module Utilities
3
+ SUPPORT_EMAIL_ADDR = "support@flucti.com"
4
+
5
+ autoload :CoreExt, 'flucti/utilities/core_ext'
6
+ autoload :UserInterface, 'flucti/utilities/user_interface'
7
+ autoload :TaskPacking, 'flucti/utilities/task_packing'
8
+ autoload :Miscellaneous, 'flucti/utilities/miscellaneous'
9
+ autoload :Table, 'flucti/utilities/table'
10
+ autoload :ListDisplayer, 'flucti/utilities/list_displayer'
11
+ autoload :ProgressBar, 'flucti/utilities/progress_bar'
12
+ autoload :ConnectionErrorHandling, 'flucti/utilities/connection_error_handling'
13
+
14
+ include UserInterface
15
+ include TaskPacking
16
+ include Miscellaneous
17
+
18
+ extend self
19
+ end
20
+ end
@@ -0,0 +1,50 @@
1
+ module Flucti
2
+ module Utilities
3
+ module ConnectionErrorHandling
4
+ def self.included(target)
5
+ target.module_eval do
6
+ alias_method_chain :request, 'exception_handling'
7
+ end
8
+ end
9
+
10
+ def request_with_exception_handling(*args, &block)
11
+ request_without_exception_handling(*args, &block)
12
+ rescue => e
13
+ Handler.handle(e) or raise e
14
+ end
15
+
16
+ module Handler
17
+ extend self, Utilities
18
+
19
+ def handle(err)
20
+ case err
21
+ when WebService::BadGateway
22
+ error! <<-MSG
23
+ The VPS you attempted to perform an action on returned an error.
24
+ It may be out of memory or disk space. Try running
25
+ #{qcommand 'vps:run CMD="vmstat -s -S M"'} and
26
+ #{qcommand 'vps:run CMD="df -h"'} respectively to determine
27
+ whether it's the case or not.
28
+ MSG
29
+ when WebService::ServiceUnavailable, WebService::TimeoutError, Errno::ECONNREFUSED
30
+ error! <<-MSG
31
+ It looks like we are currently performing maintenance on our
32
+ servers. Please retry later.
33
+ MSG
34
+ when WebService::GatewayTimeout
35
+ error! <<-MSG
36
+ The VPS you attempted to perform an action on did not respond in
37
+ time. Please check its system load averages and retry.
38
+ MSG
39
+ when WebService::ServerError
40
+ error! <<-MSG
41
+ An error occurred while processing your request. We have be
42
+ notified about this error and will work on fixing it. We
43
+ apologize for the inconvenience. Please retry later.
44
+ MSG
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,35 @@
1
+ module Flucti
2
+ module Utilities
3
+ module CoreExt
4
+ def self.apply!
5
+ # Proper version of #constantize, since ActiveSupport's behaves strangely.
6
+ String.class_eval do
7
+ def constantize
8
+ split('::').inject(Object) { |mod, name| mod.const_get(name) }
9
+ end
10
+ end
11
+
12
+ Pathname.class_eval do
13
+ alias_method :/, :+
14
+ protected :+
15
+ end
16
+
17
+ # Thorough version of `unhook!'.
18
+ ActiveSupport::Dependencies.unhook!
19
+ ActiveSupport::Dependencies::ClassConstMissing.module_eval do
20
+ def const_missing(*args)
21
+ super
22
+ end
23
+ end
24
+
25
+ ActiveSupport::Inflector.inflections do |inflect|
26
+ inflect.singular('databases', 'database')
27
+ end
28
+
29
+ WebService::RemoteCollection.class_eval do
30
+ include ConnectionErrorHandling
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end