auser-poolparty 0.0.8 → 0.0.9

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 (80) hide show
  1. data/CHANGELOG +8 -0
  2. data/README.txt +10 -10
  3. data/Rakefile +30 -21
  4. data/{web/static/site/images → assets}/clouds.png +0 -0
  5. data/bin/instance +39 -34
  6. data/bin/pool +44 -29
  7. data/bin/poolnotify +34 -0
  8. data/config/haproxy.conf +1 -1
  9. data/config/heartbeat_authkeys.conf +1 -1
  10. data/config/monit/haproxy.monit.conf +2 -1
  11. data/config/nginx.conf +1 -1
  12. data/config/reconfigure_instances_script.sh +28 -9
  13. data/config/sample-config.yml +1 -1
  14. data/lib/core/string.rb +3 -0
  15. data/lib/modules/ec2_wrapper.rb +47 -22
  16. data/lib/modules/file_writer.rb +38 -0
  17. data/lib/modules/sprinkle_overrides.rb +32 -0
  18. data/lib/modules/vlad_override.rb +5 -4
  19. data/lib/poolparty.rb +14 -10
  20. data/lib/poolparty/application.rb +33 -19
  21. data/lib/poolparty/master.rb +227 -105
  22. data/lib/poolparty/optioner.rb +8 -4
  23. data/lib/poolparty/plugin.rb +34 -4
  24. data/lib/poolparty/provider/packages/haproxy.rb +0 -15
  25. data/lib/poolparty/provider/packages/heartbeat.rb +1 -1
  26. data/lib/poolparty/provider/packages/ruby.rb +6 -6
  27. data/lib/poolparty/provider/packages/s3fuse.rb +9 -2
  28. data/lib/poolparty/provider/provider.rb +65 -25
  29. data/lib/poolparty/remote_instance.rb +95 -74
  30. data/lib/poolparty/remoter.rb +48 -37
  31. data/lib/poolparty/remoting.rb +41 -17
  32. data/lib/poolparty/scheduler.rb +4 -4
  33. data/lib/poolparty/tasks.rb +1 -1
  34. data/lib/poolparty/tasks/package.rake +53 -0
  35. data/lib/poolparty/tasks/plugins.rake +1 -1
  36. data/poolparty.gemspec +50 -58
  37. data/spec/application_spec.rb +28 -0
  38. data/spec/core_spec.rb +9 -0
  39. data/spec/ec2_wrapper_spec.rb +87 -0
  40. data/spec/file_writer_spec.rb +73 -0
  41. data/spec/files/describe_response +37 -0
  42. data/spec/files/multi_describe_response +69 -0
  43. data/spec/files/remote_desc_response +37 -0
  44. data/spec/helpers/ec2_mock.rb +3 -0
  45. data/spec/master_spec.rb +302 -78
  46. data/spec/monitors/cpu_monitor_spec.rb +2 -1
  47. data/spec/monitors/memory_spec.rb +1 -0
  48. data/spec/monitors/misc_monitor_spec.rb +1 -0
  49. data/spec/monitors/web_spec.rb +1 -0
  50. data/spec/optioner_spec.rb +12 -0
  51. data/spec/plugin_manager_spec.rb +10 -10
  52. data/spec/plugin_spec.rb +6 -3
  53. data/spec/pool_binary_spec.rb +3 -0
  54. data/spec/poolparty_spec.rb +12 -7
  55. data/spec/provider_spec.rb +1 -0
  56. data/spec/remote_instance_spec.rb +18 -18
  57. data/spec/remoter_spec.rb +4 -2
  58. data/spec/remoting_spec.rb +10 -2
  59. data/spec/scheduler_spec.rb +0 -6
  60. data/spec/spec_helper.rb +13 -0
  61. metadata +83 -52
  62. data/Manifest +0 -115
  63. data/lib/poolparty/tmp.rb +0 -46
  64. data/misc/basics_tutorial.txt +0 -142
  65. data/web/static/conf/nginx.conf +0 -22
  66. data/web/static/site/images/balloon.png +0 -0
  67. data/web/static/site/images/cb.png +0 -0
  68. data/web/static/site/images/railsconf_preso_img.png +0 -0
  69. data/web/static/site/index.html +0 -71
  70. data/web/static/site/javascripts/application.js +0 -3
  71. data/web/static/site/javascripts/corner.js +0 -178
  72. data/web/static/site/javascripts/jquery-1.2.6.pack.js +0 -11
  73. data/web/static/site/misc.html +0 -42
  74. data/web/static/site/storage/pool_party_presentation.pdf +0 -0
  75. data/web/static/site/stylesheets/application.css +0 -100
  76. data/web/static/site/stylesheets/reset.css +0 -17
  77. data/web/static/src/layouts/application.haml +0 -25
  78. data/web/static/src/pages/index.haml +0 -25
  79. data/web/static/src/pages/misc.haml +0 -5
  80. data/web/static/src/stylesheets/application.sass +0 -100
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ v0.0.9 * Changed configuration style to rsync all files across every instance
2
+ * Moved configuration back to a configure scrip
3
+ * Added in vlad configuration
4
+ * Added plugin ability to install custom software
5
+ * Updated configuration style
6
+ * Added cloud_list to pool
7
+ * Changed listing of the clouds through pool
8
+ * Added poolnotify
1
9
  v0.0.8 * Added plugin_manager
2
10
  * Moved remoting to rake remote task (from vlad)
3
11
  * Changed configuration from instance-based to cloud-based
data/README.txt CHANGED
@@ -1,4 +1,4 @@
1
- = PoolParty
1
+ = poolparty
2
2
 
3
3
  http://poolpartyrb.com
4
4
  Ari Lerner
@@ -7,19 +7,19 @@
7
7
 
8
8
  == DESCRIPTION:
9
9
 
10
- PoolParty (http://poolpartyrb.com), Ari Lerner (http://blog.xnot.org, http://blog.citrusbyte.com) - PoolParty is a framework for maintaining and running auto-scalable applications on Amazon's EC2 cloud. Run entire applications using the EC2 cluster and the unlimited S3 disk. More details to be listed at http://poolpartyrb.com.
10
+ poolparty (http://poolpartyrb.com), Ari Lerner (http://blog.xnot.org, http://blog.citrusbyte.com) - poolparty is a framework for maintaining and running auto-scalable applications on Amazon's EC2 cloud. Run entire applications using the EC2 cluster and the unlimited S3 disk. More details to be listed at http://poolpartyrb.com.
11
11
 
12
12
  == Basics
13
13
 
14
- PoolParty is written with the intention of being as application-agnostic as possible. It installs only the basic required software to glue the cloud together on the instances as listed below.
14
+ poolparty is written with the intention of being as application-agnostic as possible. It installs only the basic required software to glue the cloud together on the instances as listed below.
15
15
 
16
- PoolParty is easily configuration. In fact, it makes little assumptions about your development environment and allows several options on how to begin configuring the cloud.
16
+ poolparty is easily configuration. In fact, it makes little assumptions about your development environment and allows several options on how to begin configuring the cloud.
17
17
 
18
18
  = Development setup
19
19
 
20
20
  === IN THE ENVIRONMENT
21
21
 
22
- There are 5 values that PoolParty reads from the environment, you can set these basic environment variables and leave the rest to the PoolParty defaults. Those values are:
22
+ There are 5 values that poolparty reads from the environment, you can set these basic environment variables and leave the rest to the poolparty defaults. Those values are:
23
23
 
24
24
  ENV["ACCESS_KEY"] => AWS access key
25
25
  ENV["SECRET_ACCESS_KEY"] => AWS secret access key
@@ -31,11 +31,11 @@ The structure assumed for the keypair is EC2_HOME/id_rsa-<keypairname>
31
31
 
32
32
  === IN A CONFIG FILE
33
33
 
34
- PoolParty assumes your config directory is set in config/config.yml. However, you can set this in your environment variables and it will read the config file from the environment variable
34
+ poolparty assumes your config directory is set in config/config.yml. However, you can set this in your environment variables and it will read the config file from the environment variable
35
35
 
36
36
  === WITH A RAKE TASK
37
37
 
38
- PoolParty comes with a rake task that can setup your environment for you. Set the environment variables above and run
38
+ poolparty comes with a rake task that can setup your environment for you. Set the environment variables above and run
39
39
 
40
40
  rake dev:setup
41
41
 
@@ -47,9 +47,9 @@ and your environment will be all setup for you everytime you want to work on the
47
47
 
48
48
  = Basics
49
49
 
50
- PoolParty can work in two ways to load balance it's traffic. It can either do server-side or client-side load-balancing. Since every instance load balances itself, you can either set the client to grab an instance and send it to that using client-side load balancing (with a js library). Alternatively, you can set the master in dns and reference it when referring to the application.
50
+ poolparty can work in two ways to load balance it's traffic. It can either do server-side or client-side load-balancing. Since every instance load balances itself, you can either set the client to grab an instance and send it to that using client-side load balancing (with a js library). Alternatively, you can set the master in dns and reference it when referring to the application.
51
51
 
52
- Since PoolParty makes no assumptions as to what you will be hosting on the application, the world is your oyster when running a cloud. You can set each instance to register with a dynDNS service so that your application has multiple points of entry and can run load-balanced on the fly.
52
+ Since poolparty makes no assumptions as to what you will be hosting on the application, the world is your oyster when running a cloud. You can set each instance to register with a dynDNS service so that your application has multiple points of entry and can run load-balanced on the fly.
53
53
 
54
54
  Every instance will auto-mount the s3 bucket set in the config file (if it is set up) into the /data folder of the instance. This gives each instance access to the same data regardless of the instance. It uses s3fuse and caching through s3fuse in the /tmp directory to work as fast as possible on the local instances.
55
55
 
@@ -68,7 +68,7 @@ Each instance has a /etc/hosts file that has each node listed as the node name l
68
68
 
69
69
  = CloudSpeak - Communicating to your cloud(s)
70
70
  Binaries!
71
- Included in PoolParty are two binaries to communicate back with your clouds. Those are:
71
+ Included in poolparty are two binaries to communicate back with your clouds. Those are:
72
72
 
73
73
  * pool - operate on your pool. This includes list, start, stop maintain, restart. Check the help with pool -h
74
74
  * instance - operate on a specific instance. This allos you to ssh, scp, reload, install as well. Check the help with: instance -h
data/Rakefile CHANGED
@@ -1,27 +1,36 @@
1
1
  require 'rubygems'
2
- require 'echoe'
3
- require 'lib/poolparty'
2
+ require "lib/poolparty"
3
+ begin
4
+ require 'echoe'
5
+
6
+ Echoe.new("poolparty") do |s|
7
+ s.author = "Ari Lerner"
8
+ s.email = "ari.lerner@citrusbyte.com"
9
+ s.summary = "Run your entire application off EC2, managed and auto-scaling"
10
+ s.url = "http://blog.citrusbyte.com"
11
+ s.runtime_dependencies = ["aws-s3", "amazon-ec2", "auser-aska", "git", "crafterm-sprinkle", "SystemTimer"]
12
+ s.development_dependencies = []
13
+ s.install_message = %q{
14
+
15
+ Get ready to jump in the pool, you just installed PoolParty!
4
16
 
5
- task :default => :test
17
+ Please check out the documentation for any questions or check out the google groups at
18
+ http://groups.google.com/group/poolpartyrb
19
+
20
+ Don't forget to check out the plugin tutorial @ http://poolpartyrb.com for extending PoolParty!
6
21
 
7
- Echoe.new("poolparty") do |p|
8
- p.author = "Ari Lerner"
9
- p.email = "ari.lerner@citrusbyte.com"
10
- p.summary = "Run your entire application off EC2, managed and auto-scaling"
11
- p.url = "http://blog.citrusbyte.com"
12
- p.dependencies = %w(aws-s3 amazon-ec2 aska git)
13
- p.install_message =<<-EOM
14
- Thanks for installing PoolParty!
15
-
16
- Please check out the documentation for any questions or check out the google groups at
17
- http://groups.google.com/group/poolpartyrb
18
-
19
- Don't forget to check out the plugins for extending PoolParty!
20
-
21
- For more information, check http://poolpartyrb.com
22
- *** Ari Lerner @ <ari.lerner@citrusbyte.com> ***
23
- EOM
24
- p.include_rakefile = true
22
+ For more information, check http://poolpartyrb.com
23
+ On IRC:
24
+ irc.freenode.net
25
+ #poolpartyrb
26
+ *** Ari Lerner @ <ari.lerner@citrusbyte.com> ***
27
+ }
28
+ end
29
+
30
+ rescue LoadError => boom
31
+ puts "You are missing a dependency required for meta-operations on this gem."
25
32
  end
26
33
 
34
+ task :default => :test
35
+
27
36
  PoolParty.include_tasks
File without changes
data/bin/instance CHANGED
@@ -14,7 +14,7 @@ Usage: instance [OPTIONS] { #{commandables.join(" | ")} }
14
14
  })
15
15
  PoolParty.load_plugins
16
16
  master = PoolParty::Master.new
17
- list = PoolParty::Optioner.parse(ARGV.dup, %w(-v))
17
+ list = PoolParty::Optioner.parse(ARGV.dup, %w(-v --verbose))
18
18
  num = list.reject {|a| commandables.include?(a) }.pop
19
19
 
20
20
  instance = master.get_node(num)
@@ -24,38 +24,43 @@ unless instance
24
24
  exit
25
25
  end
26
26
 
27
- case list[0]
28
- when "ssh"
29
- PoolParty.message "Ssh'ing into #{instance.ip}"
30
- instance.ssh
31
- when "cmd"
32
- PoolParty.message "Executing #{instance_options[:cmd]} on #{instance.ip}"
33
- instance.ssh instance_options[:cmd]
34
- when "scp"
35
- instance.scp instance_options[:src], instance_options[:dest]
36
- when "restart"
37
- PoolParty.message "Restarting services"
38
- instance.restart_with_monit
39
- when "start"
40
- PoolParty.message "Starting services"
41
- instance.start_with_monit
42
- when "stop"
43
- PoolParty.message "Stopping services"
44
- instance.stop_with_monit
45
- when "install"
46
- PoolParty.message "Installing services"
47
- instance.install
48
- when "start_maintain"
49
- PoolParty.message "Running heartbeat failover service"
50
- pid = Master.run_thread_loop(:daemonize => true) do
51
- instance.become_master if instance.is_not_master_and_master_is_not_running?
27
+ list.each do |cmd|
28
+ case cmd
29
+ when "ssh"
30
+ PoolParty.message "Ssh'ing into #{instance.ip}"
31
+ instance.ssh
32
+ when "cmd"
33
+ PoolParty.message "Executing #{instance_options[:cmd]} on #{instance.ip}"
34
+ instance.ssh list.shift
35
+ when "scp"
36
+ list.shift
37
+ src, dest = list.shift, (list.shift || "~")
38
+ PoolParty.message "Scp'ing #{src} to #{dest}"
39
+ instance.scp src, (dest || "~")
40
+ when "restart"
41
+ PoolParty.message "Restarting services"
42
+ instance.restart_with_monit
43
+ when "start"
44
+ PoolParty.message "Starting services"
45
+ instance.start_with_monit
46
+ when "stop"
47
+ PoolParty.message "Stopping services"
48
+ instance.stop_with_monit
49
+ when "install"
50
+ PoolParty.message "Installing services"
51
+ instance.install
52
+ when "start_maintain"
53
+ PoolParty.message "Running heartbeat failover service"
54
+ pid = Master.run_thread_loop(:daemonize => true) do
55
+ instance.become_master if instance.is_not_master_and_master_is_not_running?
56
+ end
57
+ File.open(Application.maintain_pid_path) {|f| f.write(pid)}
58
+ when "stop_maintain"
59
+ PoolParty.message "Stopping heartbeat failover service"
60
+ pid = open(Application.maintain_pid_path).read
61
+ `kill -9 #{pid}`
62
+ FileUtils.rm Application.maintain_pid_path # Check this
63
+ else
64
+ puts master.list
52
65
  end
53
- File.open(Application.maintain_pid_path) {|f| f.write(pid)}
54
- when "stop_maintain"
55
- PoolParty.message "Stopping heartbeat failover service"
56
- pid = open(Application.maintain_pid_path).read
57
- `kill -9 #{pid}`
58
- FileUtils.rm Application.maintain_pid_path # Check this
59
- else
60
- puts master.list
61
66
  end
data/bin/pool CHANGED
@@ -11,6 +11,7 @@ Starting #{PoolParty::Application.app_name ? "#{PoolParty::Application.app_name}
11
11
  Maximum instances: #{PoolParty::Application.maximum_instances}
12
12
  Polling every: #{PoolParty::Application.polling_time}
13
13
  Keypair: #{PoolParty::Application.keypair}
14
+ Access key: #{PoolParty::Application.access_key}
14
15
  size: #{PoolParty::Application.size}
15
16
  Plugins:
16
17
  --------------
@@ -22,7 +23,7 @@ end
22
23
  # Set defaults
23
24
  options = PoolParty.options(:optsparse =>
24
25
  {:banner => <<-EOU
25
- Usage: pool [OPTIONS] {start | stop | list | maintain | restart | install | configure}
26
+ Usage: pool [OPTIONS] {start | stop | list | clouds_list | maintain | restart | install | configure | grow | shrink | ssh | switch}
26
27
  -----------------------------------------------------------------
27
28
  EOU
28
29
  })
@@ -31,32 +32,46 @@ PoolParty.load_plugins
31
32
  master = PoolParty::Master.new
32
33
  list = PoolParty::Optioner.parse(ARGV.dup, %w(-v))
33
34
 
34
- case list[0]
35
- when "start"
36
- display_config_data
37
- master.start_cloud!
38
- when "show"
39
- display_config_data
40
- when "grow"
41
- master.grow_by_one
42
- when "shrink"
43
- master.shrink_by_one
44
- when "install"
45
- master.install_cloud
46
- when "configure"
47
- master.configure_cloud
48
- when "stop"
49
- PoolParty.message "Stopping cloud"
50
- master.request_termination_of_all_instances
51
- when "list"
52
- puts master.list
53
- when "maintain"
54
- PoolParty.message "Maintaining cloud"
55
- master.start_monitor!
56
- when "restart"
57
- PoolParty.message "Restarting cloud"
58
- master.request_termination_of_all_instances
59
- master.start_cloud!
60
- else
61
- puts master.list
35
+ display_config_data
36
+
37
+ list.each do |cmd|
38
+ case cmd
39
+ when "start"
40
+ master.start_cloud!
41
+ when "show"
42
+ display_config_data
43
+ when "grow"
44
+ master.grow_by
45
+ when "shrink"
46
+ master.shrink_by
47
+ when "install"
48
+ master.install_cloud(true)
49
+ when "configure"
50
+ master.setup_cloud
51
+ when "ssh"
52
+ list.shift
53
+ PoolParty.message "Running #{list} on the cloud"
54
+ master.ssh list.shift
55
+ when "stop"
56
+ PoolParty.message "Stopping cloud"
57
+ master.request_termination_of_all_instances
58
+ when "list"
59
+ puts master.list
60
+ when "size"
61
+ puts master.nodes.size
62
+ when "clouds_list"
63
+ puts master.clouds_list
64
+ when "maintain"
65
+ PoolParty.message "Maintaining cloud"
66
+ master.start_monitor!
67
+ when "restart"
68
+ PoolParty.message "Restarting cloud"
69
+ master.request_termination_of_all_instances
70
+ master.start_cloud!
71
+ when "switch"
72
+ context = list.shift
73
+ context ? `source ~/.#{context}_pool_keys` : puts("You must supply a context to switch to")
74
+ else
75
+ puts master.list
76
+ end
62
77
  end
data/bin/poolnotify ADDED
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/ruby
2
+ require 'rubygems'
3
+ $:.unshift(File.join(File.dirname(__FILE__), "lib"))
4
+ require 'poolparty'
5
+
6
+ def notify(mess)
7
+ `/usr/local/bin/growlnotify --image #{File.expand_path(File.dirname(__FILE__))}/../assets/clouds.png -m "Your #{PoolParty.options.keypair} cloud #{mess}"`
8
+ end
9
+
10
+ @master = PoolParty::Master.new
11
+
12
+ @@size = @master.nodes.size
13
+
14
+ notify("has #{@@size} nodes")
15
+
16
+ blk = lambda {
17
+ loop do
18
+ sz = @master.nodes.size
19
+ PoolParty.message "Your cloud has #{sz} nodes from #{@@size} nodes"
20
+ unless @@size == sz
21
+ if @@size > sz
22
+ notify("shrank to #{sz} nodes")
23
+ else
24
+ notify("grew to #{sz} nodes")
25
+ end
26
+ @@size = sz
27
+ end
28
+
29
+ @master.reset!
30
+ wait 20.seconds
31
+ end
32
+ }
33
+
34
+ PoolParty::Application.production? ? daemonize(&blk) : blk.call
data/config/haproxy.conf CHANGED
@@ -23,7 +23,7 @@ defaults
23
23
 
24
24
 
25
25
  stats uri /haproxy
26
- stats realm Statistics\ for\ PoolParty
26
+ stats realm Statistics\ for\ poolparty
27
27
 
28
28
  listen web_proxy 0.0.0.0::host_port
29
29
  :servers
@@ -1,2 +1,2 @@
1
1
  auth 1
2
- 1 md5 PasswordForPoolPartyClusterApplicationButItIsOnlyADefault
2
+ 1 md5 PasswordForpoolpartyClusterApplicationButItIsOnlyADefault
@@ -4,4 +4,5 @@ check process haproxy with pidfile /var/run/haproxy.pid
4
4
  if totalmem is greater than 100.0 MB for 4 cycles then restart
5
5
  if cpu is greater than 50% for 2 cycles then alert
6
6
  if cpu is greater than 80% for 3 cycles then restart
7
- if loadavg(5min) greater than 10 for 8 cycles then restart
7
+ if loadavg(5min) greater than 10 for 8 cycles then restart
8
+ group haproxy
data/config/nginx.conf CHANGED
@@ -13,7 +13,7 @@ http {
13
13
  server {
14
14
  listen 80;
15
15
  server_name srv;
16
- root /apps/pool-party;
16
+ root /apps/poolparty;
17
17
 
18
18
  location / {
19
19
  proxy_pass http://fast_mongrels;
@@ -1,18 +1,37 @@
1
1
  #!/bin/sh
2
2
 
3
- # Reconfigure master
3
+ # Move the hosts file
4
+ echo "Moving the hosts file into place"
5
+ :move_hostfile
6
+ # Move the authkeys
7
+ echo "Configuring the authkeys"
8
+ :configure_authkeys
9
+ # Move the config file
10
+ echo "Moving custom config file for this cloud"
11
+ :move_config_file
12
+ # Reconfigure master if master?
13
+ echo "If this is the master, I'm configuring it as the master now"
4
14
  :config_master
5
- # Start this instance's master maintain script
6
- :start_pool_maintain
7
- # Make the ha.d/resource.d
8
- sudo mkdir /etc/ha.d/resource.d/
15
+ # Configure haproxy
16
+ echo "Configuring and starting haproxy"
17
+ :configure_haproxy
9
18
  # Set this hostname as appropriate in the cloud
19
+ echo "Setting new hostname"
10
20
  :set_hostname
11
21
  # Configure heartbeat
12
- sudo mkdir /etc/ha.d/resource.d/
22
+ echo "Moving all the resource.d files into place"
23
+ :configure_resource_d
13
24
  # Start heartbeat
14
- /etc/init.d/heartbeat start
25
+ echo "Configuring and starting heartbeat"
26
+ :configure_heartbeat
15
27
  # Start s3fs
16
- :start_s3fs
28
+ echo "Mounting shared drive, if shared_bucket exists in config"
29
+ :mount_s3_drive
17
30
  # Configure monit
18
- mkdir /etc/monit.d
31
+ echo "Configuring monit"
32
+ :configure_monit
33
+ # Update the plugins
34
+ echo "Updating plugins"
35
+ :update_plugins
36
+ echo "Running user tasks"
37
+ :user_tasks
@@ -12,7 +12,7 @@
12
12
  :os: ubuntu
13
13
  :host_port: 80
14
14
  :client_port: 8001
15
- :shared_bucket: "pool-party-app-data"
15
+ :shared_bucket: "poolparty-app-data"
16
16
  :services: nginx
17
17
  :environment: production
18
18
  :contract_when:
data/lib/core/string.rb CHANGED
@@ -14,6 +14,9 @@ class String
14
14
  def ^(h={})
15
15
  self.gsub(/:([\w]+)/) {h[$1.to_sym] if h.include?($1.to_sym)}
16
16
  end
17
+ def arrayable
18
+ self.strip.split(/\n/)
19
+ end
17
20
  def runnable
18
21
  self.strip.gsub(/\n/, " && ")
19
22
  end