breeze 0.0.1

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 (40) hide show
  1. data/.gitignore +6 -0
  2. data/Gemfile +4 -0
  3. data/LICENSE +22 -0
  4. data/README.md +4 -0
  5. data/Rakefile +2 -0
  6. data/bin/breeze +17 -0
  7. data/breeze.gemspec +28 -0
  8. data/lib/breeze.rb +6 -0
  9. data/lib/breeze/fog_extensions.rb +2 -0
  10. data/lib/breeze/fog_extensions/aws.rb +65 -0
  11. data/lib/breeze/initializer.rb +40 -0
  12. data/lib/breeze/tasks.rb +10 -0
  13. data/lib/breeze/tasks/app.rb +145 -0
  14. data/lib/breeze/tasks/configuration.rb +63 -0
  15. data/lib/breeze/tasks/db.rb +31 -0
  16. data/lib/breeze/tasks/dns.rb +67 -0
  17. data/lib/breeze/tasks/list.rb +74 -0
  18. data/lib/breeze/tasks/server.rb +72 -0
  19. data/lib/breeze/tasks/server/address.rb +27 -0
  20. data/lib/breeze/tasks/server/image.rb +27 -0
  21. data/lib/breeze/tasks/server/tag.rb +16 -0
  22. data/lib/breeze/veur.rb +86 -0
  23. data/lib/templates/Thorfile +122 -0
  24. data/lib/templates/maintenance.html +23 -0
  25. data/lib/templates/profiles/minimal/scripts/install_conf.sh +4 -0
  26. data/lib/templates/profiles/minimal/scripts/install_cust.sh +7 -0
  27. data/lib/templates/profiles/rails_and_image_magick/configs/database.yml +18 -0
  28. data/lib/templates/profiles/rails_and_image_magick/configs/memcached.conf +51 -0
  29. data/lib/templates/profiles/rails_and_image_magick/configs/nginx/logrotate +26 -0
  30. data/lib/templates/profiles/rails_and_image_magick/configs/nginx/monit +9 -0
  31. data/lib/templates/profiles/rails_and_image_magick/configs/nginx/nginx.conf +99 -0
  32. data/lib/templates/profiles/rails_and_image_magick/scripts/install_conf.sh +33 -0
  33. data/lib/templates/profiles/rails_and_image_magick/scripts/install_cust.sh +24 -0
  34. data/lib/templates/shared/configs/crontab +10 -0
  35. data/lib/templates/shared/configs/monitrc +248 -0
  36. data/lib/templates/shared/scripts/credentials.sh +5 -0
  37. data/lib/templates/shared/scripts/deploy.sh +43 -0
  38. data/lib/templates/shared/scripts/install.sh +38 -0
  39. data/lib/templates/user_data.sh +4 -0
  40. metadata +133 -0
@@ -0,0 +1,67 @@
1
+ module Breeze
2
+ module Dns
3
+
4
+ class Zone < Veur
5
+
6
+ desc 'create DOMAIN', 'Create a new DNS zone'
7
+ def create(domain)
8
+ zone = dns.zones.create(:domain => domain)
9
+ puts "Zone ID: #{zone.id}"
10
+ puts "Change info: #{zone.change_info}"
11
+ puts "Name servers: #{zone.nameservers}"
12
+ end
13
+
14
+ desc 'destroy ZONE_ID', 'Destroy a DNS zone'
15
+ def destroy(zone_id)
16
+ zone = dns.zones.get(zone_id)
17
+ if accept?("Destroy DNS zone and records for #{zone.domain}?")
18
+ zone.records.each(&:destroy)
19
+ zone.destroy
20
+ end
21
+ end
22
+
23
+ desc 'import ZONE_ID FILE', 'Creates dns records specified in FILE'
24
+ long_desc <<-END_DESC
25
+ FILE should be the path to a ruby file that defines DNS_RECORDS like this:
26
+ DNS_RECORDS = [
27
+ {:name => 'example.com', :type => 'A', :ip => '1.2.3.4'},
28
+ {:name => 'www.example.com', :type => 'CNAME', :ip => 'example.com'}
29
+ ]
30
+ You can also specify :ttl for each record. The default ttl is 3600 (1 hour).
31
+ END_DESC
32
+ def import(zone_id, file)
33
+ load file
34
+ zone = get_zone(zone_id)
35
+ DNS_RECORDS.each do |record_hash|
36
+ zone.records.create(record_hash)
37
+ puts record_hash.inspect
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def get_zone(id)
44
+ dns.zones.get(id)
45
+ end
46
+
47
+ end
48
+
49
+ class Record < Zone
50
+
51
+ desc 'create ZONE_ID NAME TYPE IP [TTL]', 'Create a new DNS record'
52
+ def create(zone_id, name, type, ip, ttl=3600)
53
+ get_zone(zone_id).records.create(:name => name, :type => type, :ip => ip, :ttl => ttl)
54
+ end
55
+
56
+ desc 'destroy ZONE_ID NAME [TYPE]', 'Destroy a DNS record'
57
+ method_options :force => false
58
+ def destroy(zone_id, name, type=nil)
59
+ records = get_zone(zone_id).records.select{ |r| r.name == name && (type.nil? || r.type == type) }
60
+ if force_or_accept?("Destroy #{records.size} record#{records.size == 1 ? '' : 's'}?")
61
+ records.each(&:destroy)
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,74 @@
1
+ module Breeze
2
+
3
+ class List < Veur
4
+
5
+ desc :cloud_resources, 'List all cloud resources that the current account can control with breeze'
6
+ def cloud_resources
7
+ images
8
+ servers
9
+ addresses
10
+ volumes
11
+ db_servers
12
+ dns_zones
13
+ end
14
+
15
+ desc :images, 'Describe machine images owned by Breeze::CONFIGURATION[:image_owner]'
16
+ def images
17
+ report 'MACHINE IMAGES',
18
+ ['Name or Location', 'Image ID', 'Owner', 'Image Type', 'Public'],
19
+ fog.images.all('Owner' => Breeze::CONFIGURATION[:image_owner]).map{ |i|
20
+ [i.name||i.location, i.id, i.owner_id, i.full_type, i.is_public]
21
+ }
22
+ end
23
+
24
+ desc :servers, 'Describe server instances'
25
+ def servers
26
+ report "SERVER INSTANCES",
27
+ ['Name', 'Instance ID', 'IP Address', 'Image ID', 'Instance Type', 'Availability Zone', 'State'],
28
+ fog.servers.map { |i|
29
+ [i.name, i.id, i.public_ip_address, i.image_id, i.flavor_id, i.availability_zone, i.state]
30
+ }
31
+ end
32
+
33
+ desc :addresses, 'List allocated ip addresses'
34
+ def addresses
35
+ report "ALLOCATED IP ADDRESSES",
36
+ ['Address', 'Server'],
37
+ fog.addresses.map{ |a| [a.public_ip, a.server_id] }
38
+ end
39
+
40
+ desc :volumes, 'Describe block store volumes (EBS)'
41
+ def volumes
42
+ report "VOLUMES",
43
+ ['Volume ID', 'Size', 'Status', 'Zone', 'Snapshot ID', 'Used by'],
44
+ fog.volumes.map { |v|
45
+ [v.id, v.size, v.state, v.availability_zone, v.snapshot_id, v.server_id]
46
+ }
47
+ end
48
+
49
+ desc :db_servers, 'List database servers'
50
+ def db_servers
51
+ report "DATABASE SERVERS",
52
+ ['Name', 'Type', 'Storage', 'State', 'Endpoint'],
53
+ rds.servers.map{ |s| [s.id, s.flavor_id, s.allocated_storage, s.state, s.endpoint] }
54
+ end
55
+
56
+ desc :dns_zones, 'Describe DNS zones'
57
+ def dns_zones
58
+ zones = dns.zones
59
+ zones.each(&:reload) # necessary in order to get nameservers
60
+ report "DNS ZONES",
61
+ ['Domain', 'Zone ID', 'Name servers'],
62
+ zones.map{ |z| [z.domain, z.id, z.nameservers.join(', ')] }
63
+ end
64
+
65
+ desc 'dns_records ZONE_ID', 'List all DNS records for the given zone'
66
+ def dns_records(zone_id)
67
+ zone = dns.zones.get(zone_id)
68
+ report "DNS RECORDS FOR #{zone.domain}",
69
+ ['Name', 'Type', 'TTL', 'Value'],
70
+ zone.records.map{ |r| [r.name, r.type, r.ttl, r.ip.join(', ')] }
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,72 @@
1
+ require 'resolv'
2
+
3
+ module Breeze
4
+
5
+ # Dealing with server instances.
6
+ class Server < Veur
7
+
8
+ desc 'create', 'Launch a new server instance'
9
+ method_options CONFIGURATION[:default_server_options]
10
+ def create
11
+ if options[:user_data_file]
12
+ options[:user_data] = Base64.encode64(File.read(options[:user_data_file])).strip
13
+ end
14
+ create_server(options)
15
+ end
16
+
17
+ desc 'destroy INSTANCE_ID', 'Terminate a running (or stopped) server instance'
18
+ method_options :force => false
19
+ def destroy(instance_id)
20
+ server = fog.servers.get(instance_id)
21
+ if force_or_accept?("Terminate server #{server.display_name}?")
22
+ server.destroy
23
+ end
24
+ end
25
+
26
+ private
27
+
28
+ def create_server(options=nil)
29
+ options ||= CONFIGURATION[:default_server_options]
30
+ # puts("Server options: #{options.inspect}")
31
+ server = fog.servers.create(options)
32
+ print "Launching server #{server.id}"
33
+ wait_until('running!') { server.running? }
34
+ return server
35
+ end
36
+
37
+ def wait_until_host_is_available(host)
38
+ if Resolv.getaddresses(host).empty?
39
+ print("Waiting for #{host} to resolve")
40
+ wait_until('ready!') { Resolv.getaddresses(host).any? }
41
+ end
42
+ return true if remote_is_available?(host)
43
+ print("Waiting for #{host} to accept connections")
44
+ wait_until('ready!') { remote_is_available?(host) }
45
+ end
46
+
47
+ def remote_is_available?(host)
48
+ execute(:remote_command, :command => 'exit', :host => host)
49
+ end
50
+
51
+ def remote(command, args)
52
+ args[:command] = command
53
+ execute(:remote_command, args)
54
+ end
55
+
56
+ def upload(file_pattern, args)
57
+ args[:file_pattern] = file_pattern
58
+ execute(:upload_command, args)
59
+ end
60
+
61
+ def execute(command, args)
62
+ command = CONFIGURATION[command] if command.is_a?(Symbol)
63
+ # system(command % args)
64
+ # rescue ArgumentError # for ruby 1.8 compatibility
65
+ args.each do |key, value|
66
+ command = command.gsub("%{#{key}}", value)
67
+ end
68
+ system(command)
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,27 @@
1
+ module Breeze
2
+
3
+ # Aka Amazon Elastic IP
4
+ class Server::Address < Veur
5
+
6
+ desc 'create SERVER_ID', 'Create and associate a new elastic ip'
7
+ def create(server_id)
8
+ # TODO: fog should take server_id directly when creating an address
9
+ server = fog.servers.get(server_id)
10
+ fog.addresses.create(:server => server)
11
+ end
12
+
13
+ desc 'release IP', 'Release the ip address'
14
+ method_options :force => false
15
+ def release(ip)
16
+ if force_or_accept?("Release IP #{ip}?")
17
+ fog.addresses.get(ip).destroy
18
+ end
19
+ end
20
+
21
+ desc 'associate IP NEW_SERVER_ID', 'Associate an existing IP with a new server'
22
+ def associate(ip, server_id)
23
+ fog.associate_address(server_id, ip)
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'breeze/tasks/server'
2
+
3
+ module Breeze
4
+
5
+ # Dealing with machine images.
6
+ class Server::Image < Server
7
+ desc 'create', 'Launch a server with the base image, wait for it to boot, invoke prepare_private_image(ip_address), ' +
8
+ 'stop the server, create a new machine image as a snapshot of the root device, and destroy the server.'
9
+ method_options CONFIGURATION[:default_server_options].merge(CONFIGURATION[:create_image_options])
10
+ def create
11
+ options[:block_device_mapping] = [{:device_name => '/dev/sda1', :ebs_volume_size => options.delete(:root_device_size)}]
12
+ server = create_server(options)
13
+ prepare_private_image(server.public_ip_address)
14
+ print('Stopping the server before saving a snapshot')
15
+ server.stop
16
+ wait_until('stopped!') { server.stopped? }
17
+ thor('list:images')
18
+ image = fog.images.create(:name => ask('Image name >'), :instance_id => server.id)
19
+ server.destroy
20
+ puts
21
+ puts("===> Created image #{image.id} and terminated temporary server #{server.id}.")
22
+ puts
23
+ puts("NOTICE: it may take a while before the new image shows up in list:images")
24
+ end
25
+
26
+ end
27
+ end
@@ -0,0 +1,16 @@
1
+ module Breeze
2
+
3
+ class Server::Tag < Veur
4
+
5
+ desc 'create SERVER_ID KEY VALUE', 'Create or update a tag'
6
+ def create(server_id, key, value)
7
+ fog.tags.create(:resource_id => server_id, :key => key, :value => value)
8
+ end
9
+
10
+ desc 'destroy SERVER_ID KEY', 'Delete a tag'
11
+ def destroy(server_id, key)
12
+ fog.tags.get(key).detect{ |tag| tag.resource_id == server_id }.destroy
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,86 @@
1
+ require 'breeze/fog_extensions'
2
+
3
+ module Breeze
4
+
5
+ # Thor is also known as Veur. Veur means "guard of the shrine"
6
+ # (possibly) according to Wikipedia.
7
+ class Veur < Thor
8
+
9
+ include Thor::Actions
10
+
11
+ # shorten the task names
12
+ def self.inherited(c)
13
+ c.class_eval do
14
+ namespace Thor::Util.namespace_from_thor_class(c).sub('breeze:', '')
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Thor freezes the options (don't understand why)
21
+ def options
22
+ @unfrozen_options ||= super.dup
23
+ end
24
+
25
+ # yes? in thor cannot be accepted with enter
26
+ def accept?(question)
27
+ ! (ask("#{question} [YES/no] >") =~ /n/i)
28
+ end
29
+
30
+ # don't ask questions if given the --force option
31
+ def force_or_accept?(question)
32
+ options[:force] or accept?(question)
33
+ end
34
+
35
+ # Print out dots while waiting for something.
36
+ # Usage:
37
+ # print "My task is running..."
38
+ # wait_until { my_task.completed? }
39
+ def wait_until(message='completed!')
40
+ 3.times { dot_and_sleep(1) }
41
+ dot_and_sleep(2) until yield
42
+ puts message
43
+ end
44
+
45
+ # a helper for wait_until
46
+ def dot_and_sleep(interval)
47
+ print('.')
48
+ sleep(interval)
49
+ end
50
+
51
+ # Print a table with a title and a top border of matching width.
52
+ def report(title, columns, rows)
53
+ table = capture_table([columns] + rows)
54
+ title = "=== #{title} "
55
+ title << "=" * [(table.lines.max{|s| s.size }.size - title.size), 3].max
56
+ puts title
57
+ puts table
58
+ end
59
+
60
+ # capture table in order to determine it's width
61
+ def capture_table(table)
62
+ return 'none' if table.size == 1 # the first row is for column titles
63
+ $stdout = StringIO.new # start capturing the output
64
+ print_table(table.map{ |row| row.map(&:to_s) })
65
+ output = $stdout
66
+ $stdout = STDOUT # restore normal output
67
+ return output.string
68
+ end
69
+
70
+ def fog
71
+ @fog ||= Fog::Compute.new(CONFIGURATION[:cloud_service])
72
+ end
73
+
74
+ def dns
75
+ @dns ||= Fog::DNS.new(CONFIGURATION[:cloud_service])
76
+ end
77
+
78
+ def rds
79
+ return @rds if @rds
80
+ credentials = CONFIGURATION[:cloud_service].reject{ |k,v| k == :provider }
81
+ credentials[:region] = CONFIGURATION[:db_region]
82
+ @rds = Fog::AWS::RDS.new(credentials)
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,122 @@
1
+ # Sign up for AWS credentials at http://aws.amazon.com/, and learn how to
2
+ # upload your own public ssh key at http://alestic.com/2010/10/ec2-ssh-keys.
3
+ # Use environment variables in this file if you share it with others.
4
+
5
+ require 'breeze'
6
+
7
+ Breeze::CONFIGURATION = {
8
+
9
+ # Access credentials are needed for all tasks.
10
+ :cloud_service => {
11
+ :provider => 'AWS',
12
+ :aws_access_key_id => 'YOUR-ACCESS-KEY-ID',
13
+ :aws_secret_access_key => 'YOUR-SECTET-ACCESS-KEY'
14
+ },
15
+
16
+ # Remote and upload commands are required in order to create a server image or deploy
17
+ # an application. Arguments marked with %{} are automatically replaced when the
18
+ # commands are used. Use "ssh -i /path/to/key" if not using your default ssh key.
19
+ :remote_command => "ssh ubuntu@%{host} '%{command}'",
20
+ :upload_command => 'rsync -v %{file_pattern} ubuntu@%{host}:%{remote_path}',
21
+
22
+ # :rollback_window specifies the number of minutes to keep old instances running after new ones
23
+ # have been deployed. Rollback is no longer possible when the old instances have been destroyed.
24
+ :rollback_window => 60,
25
+
26
+ # Default server options are needed when launching new servers.
27
+ :default_server_options => {
28
+ :image_id => 'YOUR-PRIVATE-AMI-OR-A-PUBLIC-ONE',
29
+ :key_name => 'THE-NAME-OF-YOUR-KEYPAIR', # http://alestic.com/2010/10/ec2-ssh-keys
30
+ :flavor_id => 't1.micro', # t1.micro m1.small c1.medium ...
31
+ :availability_zone => 'us-east-1a', # us-west-1a eu-west-1a ap-southeast-1a
32
+ :user_data_file => nil
33
+ },
34
+
35
+ # Override default_server_options when building your own private server image:
36
+ :create_image_options => {
37
+ :image_id => 'ami-ccf405a5', # a base AMI from http://alestic.com
38
+ :root_device_size => 15 # in GB
39
+ },
40
+
41
+ # machine images owned by this account are included in list:images (provided that you have access to them)
42
+ :image_owner => 'YOUR-ACCOUNT-ID-WITHOUT-DASHES', # canonical: '099720109477'
43
+
44
+ # db configuration is required in order to use Amazon RDS
45
+ :db_region => 'us-east-1', # us-west-1 eu-west-1 ap-southeast-1
46
+ :default_db_options => {
47
+ :engine => 'mysql',
48
+ :engine_version => '5.5',
49
+ :auto_minor_version_upgrade => true,
50
+ :allocated_storage => 5, # 5 - 1024 GB
51
+ :availability_zone => 'us-east-1a',
52
+ :backup_retention_period => 1, # 0 - 8 days
53
+ :preferred_backup_window => '05:30-06:00', # daily, times in UTC
54
+ :preferred_maintenance_window => 'sun:06:00-sun:06:30', # weekly
55
+ :master_username => 'admin',
56
+ :password => 'admin'
57
+ },
58
+
59
+ # you can add your own keys and access this hash as CONFIGURATION in the erb templates
60
+ :admin_email => 'YOUR-EMAIL',
61
+ :app_path => '/srv/YOUR-APP'
62
+ }.freeze
63
+
64
+ # Define Breeze::CONFIGURATION before requiring the tasks.
65
+ # This allows us to pick up default options that can be viewed
66
+ # with the "thor help" command.
67
+ require 'breeze/tasks'
68
+
69
+ class Breeze::Server::Image
70
+ private
71
+ # This method is called from server:image:create. Modify this and/or the scripts that
72
+ # are uploaded in order to create your own private machine image.
73
+ def prepare_private_image(ip_address)
74
+ wait_until_host_is_available(ip_address)
75
+ puts("Uploading scripts...")
76
+ upload('config/breeze/scripts/*.sh', :host => ip_address, :remote_path => './')
77
+ remote('chmod 600 credentials.sh; chmod 700 deploy.sh; bash install.sh', :host => ip_address)
78
+ end
79
+ end
80
+
81
+ # You can define staging:start etc. with fixed parameters below.
82
+
83
+ class Breeze::App
84
+ private
85
+ def deploy_command(servers, public_server_name, db_server_name, branch)
86
+ servers.each do |server|
87
+ wait_until_host_is_available(ip(server))
88
+ remote("/home/ubuntu/deploy.sh #{public_server_name} #{db_endpoint(db_server_name)} #{branch}", :host => ip(server))
89
+ end
90
+ end
91
+ end
92
+
93
+ # thor staging:start and staging:stop
94
+ class Staging < Breeze::App
95
+
96
+ PUBLIC_SERVER_NAME = 'staging.example.com'
97
+ DB_SERVER_NAME = 'staging-db'
98
+
99
+ desc 'start', 'Start web server and db for staging'
100
+ def start
101
+ thor("app:start #{PUBLIC_SERVER_NAME} #{DB_SERVER_NAME} --db-to-clone=#{Production::DB_SERVER_NAME} --deploy-branch=master")
102
+ end
103
+
104
+ desc 'stop', 'Stop staging and destroy server and db'
105
+ def stop
106
+ thor("app:stop #{PUBLIC_SERVER_NAME} --force")
107
+ end
108
+
109
+ end
110
+
111
+ # thor production:start
112
+ class Production < Breeze::App
113
+
114
+ PUBLIC_SERVER_NAME = 'production.example.com'
115
+ DB_SERVER_NAME = 'production-db'
116
+
117
+ desc 'start', 'Start web server and db for production'
118
+ def start
119
+ thor("app:start #{PUBLIC_SERVER_NAME} #{DB_SERVER_NAME} crags --dns-ttl=300 --deploy-branch=stable")
120
+ end
121
+
122
+ end