bosh-cloudfoundry 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +6 -0
  4. data/Gemfile +6 -0
  5. data/Guardfile +10 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +242 -0
  8. data/Rakefile +33 -0
  9. data/bosh-cloudfoundry.gemspec +29 -0
  10. data/config/defaults.yml +6 -0
  11. data/lib/bosh-cloudfoundry.rb +34 -0
  12. data/lib/bosh-cloudfoundry/bosh_release_manager.rb +141 -0
  13. data/lib/bosh-cloudfoundry/config.rb +6 -0
  14. data/lib/bosh-cloudfoundry/config/common_config.rb +27 -0
  15. data/lib/bosh-cloudfoundry/config/dea_config.rb +150 -0
  16. data/lib/bosh-cloudfoundry/config/microbosh_config.rb +106 -0
  17. data/lib/bosh-cloudfoundry/config/postgresql_service_config.rb +185 -0
  18. data/lib/bosh-cloudfoundry/config/redis_service_config.rb +179 -0
  19. data/lib/bosh-cloudfoundry/config/system_config.rb +60 -0
  20. data/lib/bosh-cloudfoundry/config_options.rb +362 -0
  21. data/lib/bosh-cloudfoundry/gerrit_patches_helper.rb +47 -0
  22. data/lib/bosh-cloudfoundry/providers.rb +19 -0
  23. data/lib/bosh-cloudfoundry/providers/aws.rb +75 -0
  24. data/lib/bosh-cloudfoundry/system_deployment_manifest_renderer.rb +286 -0
  25. data/lib/bosh-cloudfoundry/version.rb +5 -0
  26. data/lib/bosh/cli/commands/cf.rb +668 -0
  27. data/spec/assets/.gitkeep +0 -0
  28. data/spec/assets/cf-release/jobs/cloud_controller/templates/runtimes.yml +150 -0
  29. data/spec/assets/deployments/aws-core-1-m1.small-free-redis.yml +221 -0
  30. data/spec/assets/deployments/aws-core-1-m1.xlarge-free-postgresql-2-m1.small-free-postgresql.yml +248 -0
  31. data/spec/assets/deployments/aws-core-2-m1.xlarge-dea.yml +195 -0
  32. data/spec/assets/deployments/aws-core-only.yml +178 -0
  33. data/spec/assets/deployments/aws-core-with-nfs.yml +192 -0
  34. data/spec/functional/.gitkeep +0 -0
  35. data/spec/spec_helper.rb +41 -0
  36. data/spec/unit/.gitkeep +0 -0
  37. data/spec/unit/bosh_release_manager_spec.rb +36 -0
  38. data/spec/unit/cf_command_spec.rb +280 -0
  39. data/spec/unit/config/common_config_spec.rb +21 -0
  40. data/spec/unit/config/dea_config_spec.rb +92 -0
  41. data/spec/unit/config/microbosh_config_spec.rb +11 -0
  42. data/spec/unit/config/postgresql_service_config_spec.rb +103 -0
  43. data/spec/unit/config/redis_service_config_spec.rb +103 -0
  44. data/spec/unit/config/system_config_spec.rb +29 -0
  45. data/spec/unit/config_options_spec.rb +64 -0
  46. data/spec/unit/system_deployment_manifest_renderer_spec.rb +93 -0
  47. metadata +246 -0
@@ -0,0 +1,47 @@
1
+ # Copyright (c) 2012-2013 Stark & Wayne, LLC
2
+
3
+ module Bosh; module CloudFoundry; end; end
4
+
5
+ # There are two concepts of "latest".
6
+ # * for upload: "latest" is the highest release in cf-release
7
+ # * for manifest creation: "latest" is the highest release already uploaded to the BOSH
8
+ module Bosh::CloudFoundry::GerritPatchesHelper
9
+
10
+ def extract_refs_change(gerrit_change)
11
+ if gerrit_change =~ %r{(\d+)/(\d+)/(\d+)$}
12
+ "#{$1}/#{$2}/#{$3}"
13
+ else
14
+ nil
15
+ end
16
+ end
17
+
18
+ def add_gerrit_refs_change(refs_change)
19
+ system_config.gerrit_changes ||= []
20
+ unless system_config.gerrit_changes.include?(refs_change)
21
+ system_config.gerrit_changes << refs_change
22
+ system_config.save
23
+ end
24
+ end
25
+
26
+ def apply_gerrit_patches
27
+ # is the gerrit setup necessary; or can use anonymous HTTP?
28
+ # confirm_gerrit_username # http://reviews.cloudfoundry.org/#/settings/
29
+ # confirm_user_added_vcap_ssh_keys_to_gerrit # http://reviews.cloudfoundry.org/#/settings/ssh-keys
30
+ # confirm_ssh_access # ssh -p 29418 drnic@reviews.cloudfoundry.org 2>&1 | grep "Permission denied"
31
+ create_and_change_into_patches_branch
32
+ ssh_uri = "http://reviews.cloudfoundry.org/cf-release"
33
+ chdir(cf_release_dir) do
34
+ system_config.gerrit_changes.each do |refs_change|
35
+ sh "git pull #{ssh_uri} refs/changes/#{refs_change}"
36
+ end
37
+ end
38
+ end
39
+
40
+ def create_and_change_into_patches_branch
41
+ chdir(cf_release_dir) do
42
+ sh "git checkout master"
43
+ sh "git branch -f patches" # force create
44
+ sh "git checkout patches"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,19 @@
1
+ # Copyright (c) 2012-2013 Stark & Wayne, LLC
2
+
3
+ module Bosh; module CloudFoundry; end; end
4
+
5
+ module Bosh::CloudFoundry::Providers
6
+ extend self
7
+ # returns a BOSH provider (CPI) specific object
8
+ # with helpers related to that provider
9
+ def for_bosh_provider_name(system_config)
10
+ case system_config.bosh_provider.to_sym
11
+ when :aws
12
+ Bosh::CloudFoundry::Providers::AWS.new(system_config.microbosh.fog_compute)
13
+ else
14
+ raise "please support #{system_config.bosh_provider} provider"
15
+ end
16
+ end
17
+ end
18
+
19
+ require "bosh-cloudfoundry/providers/aws"
@@ -0,0 +1,75 @@
1
+ # Copyright (c) 2012-2013 Stark & Wayne, LLC
2
+
3
+ module Bosh; module CloudFoundry; module Providers; end; end; end
4
+
5
+ class Bosh::CloudFoundry::Providers::AWS
6
+ attr_reader :fog_compute
7
+ def initialize(fog_compute=nil)
8
+ @fog_compute = fog_compute
9
+ end
10
+
11
+ # @return [Integer] megabytes of RAM for requested flavor of server
12
+ def ram_for_server_flavor(server_flavor_id)
13
+ if flavor = fog_compute_flavor(server_flavor_id)
14
+ flavor[:ram]
15
+ else
16
+ raise "Unknown AWS flavor '#{server_flavor_id}'"
17
+ end
18
+ end
19
+
20
+ # @return [Hash] e.g. { :bits => 0, :cores => 2, :disk => 0,
21
+ # :id => 't1.micro', :name => 'Micro Instance', :ram => 613}
22
+ # or nil if +server_flavor_id+ is not a supported flavor ID
23
+ def fog_compute_flavor(server_flavor_id)
24
+ aws_compute_flavors.find { |fl| fl[:id] == server_flavor_id }
25
+ end
26
+
27
+ # @return [Array] of [Hash] for each supported compute flavor
28
+ # Example [Hash] { :bits => 0, :cores => 2, :disk => 0,
29
+ # :id => 't1.micro', :name => 'Micro Instance', :ram => 613}
30
+ def aws_compute_flavors
31
+ Fog::Compute::AWS::FLAVORS
32
+ end
33
+
34
+ # @return [String] provisions a new public IP address in target region
35
+ # TODO nil if none available
36
+ def provision_public_ip_address
37
+ return unless fog_compute
38
+ address = fog_compute.addresses.create
39
+ address.public_ip
40
+ # TODO catch error and return nil
41
+ end
42
+
43
+ # Creates or reuses an AWS security group and opens ports.
44
+ #
45
+ # +security_group_name+ is the name to be created or reused
46
+ # +ports+ is a hash of name/port for ports to open, for example:
47
+ # {
48
+ # ssh: 22,
49
+ # http: 80,
50
+ # https: 443
51
+ # }
52
+ def create_security_group(security_group_name, ports)
53
+ unless sg = fog_compute.security_groups.get(security_group_name)
54
+ sg = fog_compute.security_groups.create(name: security_group_name, description: "microbosh")
55
+ puts "Created security group #{security_group_name}"
56
+ else
57
+ puts "Reusing security group #{security_group_name}"
58
+ end
59
+ ip_permissions = sg.ip_permissions
60
+ ports_opened = 0
61
+ ports.each do |name, port|
62
+ unless port_open?(ip_permissions, port)
63
+ sg.authorize_port_range(port..port)
64
+ puts " -> opened #{name} port #{port}"
65
+ ports_opened += 1
66
+ end
67
+ end
68
+ puts " -> no additional ports opened" if ports_opened == 0
69
+ true
70
+ end
71
+
72
+ def port_open?(ip_permissions, port)
73
+ ip_permissions && ip_permissions.find {|ip| ip["fromPort"] <= port && ip["toPort"] >= port }
74
+ end
75
+ end
@@ -0,0 +1,286 @@
1
+ # Copyright (c) 2012-2013 Stark & Wayne, LLC
2
+
3
+ module Bosh; module CloudFoundry; end; end
4
+
5
+ # Renders a +SystemConfig+ model into a System's BOSH deployment
6
+ # manifest(s).
7
+ class Bosh::CloudFoundry::SystemDeploymentManifestRenderer
8
+ include FileUtils
9
+ attr_reader :system_config, :common_config, :bosh_config
10
+
11
+ def initialize(system_config, common_config, bosh_config)
12
+ @system_config = system_config
13
+ @common_config = common_config
14
+ @bosh_config = bosh_config
15
+ end
16
+
17
+ # Render deployment manifest(s) for a system
18
+ # based on the model data in +system_config+
19
+ # (a +SystemConfig+ object).
20
+ def perform
21
+ validate_system_config
22
+
23
+ deployment_name = "#{system_config.system_name}-core"
24
+
25
+ manifest = base_manifest(
26
+ deployment_name,
27
+ bosh_config.target_uuid,
28
+ system_config.bosh_provider,
29
+ system_config.system_name,
30
+ system_config.release_name,
31
+ system_config.release_version,
32
+ system_config.stemcell_name,
33
+ system_config.stemcell_version,
34
+ cloud_properties_for_server_flavor(system_config.core_server_flavor),
35
+ system_config.core_ip,
36
+ system_config.root_dns,
37
+ system_config.admin_emails,
38
+ system_config.common_password,
39
+ system_config.common_persistent_disk,
40
+ system_config.security_group
41
+ )
42
+
43
+ dea_config.add_core_jobs_to_manifest(manifest)
44
+ dea_config.add_resource_pools_to_manifest(manifest)
45
+ dea_config.add_jobs_to_manifest(manifest)
46
+ dea_config.merge_manifest_properties(manifest)
47
+
48
+ postgresql_service_config.add_core_jobs_to_manifest(manifest)
49
+ postgresql_service_config.add_resource_pools_to_manifest(manifest)
50
+ postgresql_service_config.add_jobs_to_manifest(manifest)
51
+ postgresql_service_config.merge_manifest_properties(manifest)
52
+
53
+ redis_service_config.add_core_jobs_to_manifest(manifest)
54
+ redis_service_config.add_resource_pools_to_manifest(manifest)
55
+ redis_service_config.add_jobs_to_manifest(manifest)
56
+ redis_service_config.merge_manifest_properties(manifest)
57
+
58
+ chdir system_config.system_dir do
59
+ mkdir_p("deployments")
60
+ File.open("deployments/#{system_config.system_name}-core.yml", "w") do |file|
61
+ file << manifest.to_yaml
62
+ end
63
+ # `open "deployments/#{system_config.system_name}-core.yml"`
64
+ end
65
+ end
66
+
67
+ def validate_system_config
68
+ s = system_config
69
+ must_not_be_nil = [
70
+ :system_dir,
71
+ :bosh_provider,
72
+ :release_name,
73
+ :release_version,
74
+ :stemcell_name,
75
+ :stemcell_version,
76
+ :core_server_flavor,
77
+ :core_ip,
78
+ :root_dns,
79
+ :admin_emails,
80
+ :common_password,
81
+ :common_persistent_disk,
82
+ :security_group,
83
+ ]
84
+ must_not_be_nil_failures = must_not_be_nil.inject([]) do |list, attribute|
85
+ list << attribute unless system_config.send(attribute)
86
+ list
87
+ end
88
+ if must_not_be_nil_failures.size > 0
89
+ raise "These SystemConfig fields must not be nil: #{must_not_be_nil_failures.inspect}"
90
+ end
91
+ end
92
+
93
+ def dea_config
94
+ @dea_config ||= Bosh::CloudFoundry::Config::DeaConfig.build_from_system_config(system_config)
95
+ end
96
+
97
+ def postgresql_service_config
98
+ @postgresql_service_config ||=
99
+ Bosh::CloudFoundry::Config::PostgresqlServiceConfig.build_from_system_config(system_config)
100
+ end
101
+
102
+ def redis_service_config
103
+ @redis_service_config ||=
104
+ Bosh::CloudFoundry::Config::RedisServiceConfig.build_from_system_config(system_config)
105
+ end
106
+
107
+ # Converts a server flavor (such as 'm1.large' on AWS) into
108
+ # a BOSH deployment manifest +cloud_properties+ YAML string
109
+ # For AWS & m1.large, it would be:
110
+ # 'instance_type: m1.large'
111
+ def cloud_properties_for_server_flavor(server_flavor)
112
+ if aws?
113
+ { "instance_type" => server_flavor }
114
+ else
115
+ raise 'Please implement #{self.class}#cloud_properties_for_server_flavor'
116
+ end
117
+ end
118
+
119
+ def aws?
120
+ system_config.bosh_provider == "aws"
121
+ end
122
+
123
+ #
124
+ def base_manifest(
125
+ deployment_name,
126
+ director_uuid,
127
+ bosh_provider,
128
+ system_name,
129
+ release_name,
130
+ release_version,
131
+ stemcell_name,
132
+ stemcell_version,
133
+ core_cloud_properties,
134
+ core_ip,
135
+ root_dns,
136
+ admin_emails,
137
+ common_password,
138
+ common_persistent_disk,
139
+ security_group
140
+ )
141
+ # This large, terse, pretty-printed manifest can be
142
+ # generated by loading in a spec/assets/deployments/*.yml file
143
+ # and pretty-printing it.
144
+ #
145
+ # manifest = YAML.load_file('spec/assets/deployments/aws-core-only.yml')
146
+ # require "pp"
147
+ # pp manifest
148
+ {"name"=>deployment_name,
149
+ "director_uuid"=>director_uuid,
150
+ "release"=>{"name"=>release_name, "version"=>release_version},
151
+ "compilation"=>
152
+ {"workers"=>10,
153
+ "network"=>"default",
154
+ "reuse_compilation_vms"=>true,
155
+ "cloud_properties"=>{"instance_type"=>"m1.medium"}},
156
+ "update"=>
157
+ {"canaries"=>1,
158
+ "canary_watch_time"=>"30000-150000",
159
+ "update_watch_time"=>"30000-150000",
160
+ "max_in_flight"=>4,
161
+ "max_errors"=>1},
162
+ "networks"=>
163
+ [{"name"=>"default",
164
+ "type"=>"dynamic",
165
+ "cloud_properties"=>{"security_groups"=>[security_group]}},
166
+ {"name"=>"vip_network",
167
+ "type"=>"vip",
168
+ "cloud_properties"=>{"security_groups"=>[security_group]}}],
169
+ "resource_pools"=>
170
+ [{"name"=>"core",
171
+ "network"=>"default",
172
+ "size"=>1,
173
+ "stemcell"=>{"name"=>stemcell_name, "version"=>stemcell_version},
174
+ "cloud_properties"=>core_cloud_properties,
175
+ "persistent_disk"=>common_persistent_disk}],
176
+ "jobs"=>
177
+ [{"name"=>"core",
178
+ "template"=>
179
+ ["postgres",
180
+ "nats",
181
+ "router",
182
+ "health_manager",
183
+ "cloud_controller",
184
+ # "debian_nfs_server",
185
+ # "serialization_data_server",
186
+ "stager",
187
+ "uaa",
188
+ "vcap_redis"],
189
+ "instances"=>1,
190
+ "resource_pool"=>"core",
191
+ "networks"=>
192
+ [{"name"=>"default", "default"=>["dns", "gateway"]},
193
+ {"name"=>"vip_network", "static_ips"=>[core_ip]}],
194
+ "persistent_disk"=>common_persistent_disk}],
195
+ "properties"=>
196
+ {"domain"=>root_dns,
197
+ "env"=>nil,
198
+ "networks"=>{"apps"=>"default", "management"=>"default"},
199
+ "router"=>
200
+ {"client_inactivity_timeout"=>600,
201
+ "app_inactivity_timeout"=>600,
202
+ "local_route"=>core_ip,
203
+ "status"=>
204
+ {"port"=>8080, "user"=>"router", "password"=>common_password}},
205
+ "nats"=>
206
+ {"user"=>"nats",
207
+ "password"=>common_password,
208
+ "address"=>core_ip,
209
+ "port"=>4222},
210
+ "db"=>"ccdb",
211
+ "ccdb"=>
212
+ {"template"=>"postgres",
213
+ "address"=>core_ip,
214
+ "port"=>2544,
215
+ "databases"=>
216
+ [{"tag"=>"cc", "name"=>"appcloud"},
217
+ {"tag"=>"uaa", "name"=>"uaa"}],
218
+ "roles"=>
219
+ [{"name"=>"root", "password"=>common_password, "tag"=>"admin"},
220
+ {"name"=>"uaa", "password"=>common_password, "tag"=>"uaa"}]},
221
+ "cc"=>
222
+ {"description"=>"Cloud Foundry",
223
+ "srv_api_uri"=>"http://api.#{root_dns}",
224
+ "password"=>common_password,
225
+ "token"=>"TOKEN",
226
+ "allow_debug"=>true,
227
+ "allow_registration"=>true,
228
+ "admins"=>admin_emails,
229
+ "admin_account_capacity"=>
230
+ {"memory"=>2048, "app_uris"=>32, "services"=>16, "apps"=>16},
231
+ "default_account_capacity"=>
232
+ {"memory"=>2048, "app_uris"=>32, "services"=>16, "apps"=>16},
233
+ "new_stager_percent"=>100,
234
+ "staging_upload_user"=>"vcap",
235
+ "staging_upload_password"=>common_password,
236
+ "uaa"=>
237
+ {"enabled"=>true,
238
+ "resource_id"=>"cloud_controller",
239
+ "token_creation_email_filter"=>[""]},
240
+ "service_extension"=>{"service_lifecycle"=>{"max_upload_size"=>5}},
241
+ "use_nginx"=>false},
242
+ "postgresql_server"=>{"max_connections"=>30, "listen_address"=>"0.0.0.0"},
243
+ # "serialization_data_server"=>
244
+ # {"upload_token"=>"TOKEN",
245
+ # "use_nginx"=>false,
246
+ # "upload_timeout"=>10,
247
+ # "port"=>8090,
248
+ # "upload_file_expire_time"=>600,
249
+ # "purge_expired_interval"=>30},
250
+ "service_lifecycle"=>
251
+ {"download_url"=>core_ip,
252
+ "mount_point"=>"/var/vcap/service_lifecycle",
253
+ "tmp_dir"=>"/var/vcap/service_lifecycle/tmp_dir",
254
+ "resque"=>
255
+ {"host"=>core_ip, "port"=>3456, "password"=>common_password},
256
+ # "nfs_server"=>{"address"=>core_ip, "export_dir"=>"/cfsnapshot"},
257
+ # "serialization_data_server"=>[core_ip]
258
+ },
259
+ "stager"=>
260
+ {"max_staging_duration"=>120,
261
+ "max_active_tasks"=>20,
262
+ "queues"=>["staging"]},
263
+ "uaa"=>
264
+ {"cc"=>{"token_secret"=>"TOKEN_SECRET", "client_secret"=>"CLIENT_SECRET"},
265
+ "admin"=>{"client_secret"=>"CLIENT_SECRET"},
266
+ "login"=>{"client_secret"=>"CLIENT_SECRET"},
267
+ "batch"=>{"username"=>"uaa", "password"=>common_password},
268
+ "port"=>8100,
269
+ "catalina_opts"=>"-Xmx128m -Xms30m -XX:MaxPermSize=128m",
270
+ "no_ssl"=>true},
271
+ "uaadb"=>
272
+ {"address"=>core_ip,
273
+ "port"=>2544,
274
+ "roles"=>
275
+ [{"tag"=>"admin", "name"=>"uaa", "password"=>common_password}],
276
+ "databases"=>[{"tag"=>"uaa", "name"=>"uaa"}]},
277
+ "vcap_redis"=>
278
+ {"address"=>core_ip,
279
+ "port"=>3456,
280
+ "password"=>common_password,
281
+ "maxmemory"=>500000000},
282
+ "service_plans"=>{},
283
+ "dea"=>{"max_memory"=>512}}}
284
+ end
285
+
286
+ end
@@ -0,0 +1,5 @@
1
+ module Bosh
2
+ module Cloudfoundry
3
+ VERSION = "0.2.0"
4
+ end
5
+ end
@@ -0,0 +1,668 @@
1
+ # Copyright (c) 2012-2013 Stark & Wayne, LLC
2
+
3
+ require 'bosh-cloudfoundry'
4
+
5
+ module Bosh::Cli::Command
6
+ class CloudFoundry < Base
7
+ include Bosh::Cli::DeploymentHelper
8
+ include Bosh::Cli::VersionCalc
9
+ include Bosh::CloudFoundry::ConfigOptions
10
+ include Bosh::CloudFoundry::BoshReleaseManager
11
+ include Bosh::CloudFoundry::GerritPatchesHelper
12
+ include FileUtils
13
+
14
+ usage "cf"
15
+ desc "show cf bosh sub-commands"
16
+ def cf_help
17
+ say("bosh cf sub-commands:")
18
+ nl
19
+ cmds = Bosh::Cli::Config.commands.values.find_all {|c|
20
+ c.usage =~ /^cf/
21
+ }
22
+ Bosh::Cli::Command::Help.list_commands(cmds)
23
+ end
24
+
25
+ usage "cf prepare system"
26
+ desc "create CloudFoundry system"
27
+ option "--core-ip ip", String, "Static IP for CloudController/router, e.g. 1.2.3.4"
28
+ option "--root-dns dns", String, "Base DNS for CloudFoundry applications, e.g. vcap.me"
29
+ option "--core-server-flavor flavor", String,
30
+ "Flavor of the CloudFoundry Core server. Default: 'm1.large'"
31
+ option "--release-name name", String,
32
+ "Name of BOSH release within target BOSH. Default: 'appcloud'"
33
+ option "--release-version version", String,
34
+ "Version of target BOSH release within target BOSH. Default: 'latest'"
35
+ option "--stemcell-name name", String,
36
+ "Name of BOSH stemcell within target BOSH. Default: 'bosh-stemcell'"
37
+ option "--stemcell-version version", String,
38
+ "Version of BOSH stemcell within target BOSH. Default: determines latest for stemcell"
39
+ option "--admin-emails email1,email2", Array, "Admin email accounts in created CloudFoundry"
40
+ option "--skip-validations", "Skip all validations"
41
+ def prepare_system(name=nil)
42
+ setup_system_dir(name)
43
+ confirm_or_prompt_all_defaults
44
+ confirm_or_prompt_for_system_requirements
45
+ render_system
46
+ target_core_deployment_manifest
47
+ end
48
+
49
+ usage "cf change deas"
50
+ desc "change the number/flavor of DEA servers (servers that run CF apps)"
51
+ option "--flavor flavor", String, "Change flavor of all DEA servers"
52
+ def change_deas(server_count="1")
53
+ confirm_system
54
+
55
+ server_count = server_count.to_i # TODO nicer integer validation
56
+ if server_count <= 0
57
+ say "Additional server count (#{server_count}) was less that 1, defaulting to 1"
58
+ server_count = 1
59
+ end
60
+
61
+ server_flavor = options[:flavor]
62
+ unless non_interactive?
63
+ unless server_flavor
64
+ server_flavor = ask("Flavor of server for DEAs? ") do |q|
65
+ q.default = default_dea_server_flavor
66
+ end.to_s
67
+ end
68
+ end
69
+ unless server_flavor && server_flavor
70
+ err("Must provide server count and flavor values")
71
+ end
72
+ validate_compute_flavor(server_flavor)
73
+
74
+ dea_config = Bosh::CloudFoundry::Config::DeaConfig.build_from_system_config(system_config)
75
+ dea_config.update_count_and_flavor(server_count, server_flavor)
76
+
77
+ render_system
78
+ end
79
+
80
+ usage "cf add service"
81
+ desc "add additional CloudFoundry service node"
82
+ option "--flavor flavor", String, "Server flavor for additional service nodes"
83
+ def add_service_node(service_name, additional_count=1)
84
+ confirm_system
85
+
86
+ validate_service_name(service_name)
87
+
88
+ server_flavor = options[:flavor]
89
+ unless non_interactive?
90
+ unless server_flavor
91
+ server_flavor = ask("Flavor of server for #{service_name} service nodes? ") do |q|
92
+ q.default = default_service_server_flavor(service_name)
93
+ end
94
+ end
95
+ end
96
+ unless server_flavor && server_flavor
97
+ err("Must provide server count and flavor values")
98
+ end
99
+ validate_compute_flavor(server_flavor)
100
+
101
+ service_config = service_config(service_name)
102
+ flavor_cluster = service_config.find_cluster_for_flavor(server_flavor) || {}
103
+
104
+ current_count = flavor_cluster["count"] || 0
105
+ server_count = current_count + additional_count.to_i # TODO nicer integer validation
106
+ say "Changing #{service_name} #{server_flavor} from #{current_count} to #{server_count}"
107
+ service_config.update_cluster_count_for_flavor(server_count, server_flavor)
108
+
109
+ render_system
110
+ end
111
+
112
+ usage "cf upload stemcell"
113
+ desc "download/create stemcell & upload to BOSH"
114
+ option "--latest", "Use latest stemcell; possibly not tagged stable"
115
+ option "--custom", "Create custom stemcell from BOSH git source"
116
+ def upload_stemcell
117
+ stemcell_type = "stable"
118
+ stemcell_type = "latest" if options[:latest]
119
+ stemcell_type = "custom" if options[:custom]
120
+ create_or_download_stemcell_then_upload(stemcell_type)
121
+ end
122
+
123
+ usage "cf upload release"
124
+ desc "fetch & upload latest public cloudfoundry release to BOSH"
125
+ option "--dev", "Create development release from very latest cf-release commits"
126
+ def upload_release
127
+ clone_or_update_cf_release
128
+ if options.delete(:dev)
129
+ create_and_upload_dev_release
130
+ else
131
+ upload_final_release
132
+ end
133
+ end
134
+
135
+ usage "cf merge gerrit"
136
+ desc "create development release including one or more gerrit patches"
137
+ def merge_gerrit(*gerrit_changes)
138
+ # gerrit_change might be:
139
+ # * refs/changes/84/13084/4
140
+ # * 84/13084/4
141
+ # * 'git pull http://reviews.cloudfoundry.org/cf-release refs/changes/84/13084/4'
142
+ gerrit_changes.each do |gerrit_change|
143
+ if refs_change = extract_refs_change(gerrit_change)
144
+ add_gerrit_refs_change(refs_change)
145
+ else
146
+ say "Please provide the gerrit change information in one of the following formats:".red
147
+ say " -> bosh cf merge gerrit refs/changes/84/13084/4"
148
+ say " -> bosh cf merge gerrit 84/13084/4"
149
+ say " -> bosh cf merge gerrit 'git pull http://reviews.cloudfoundry.org/cf-release refs/changes/84/13084/4'"
150
+ say ""
151
+ say "Please re-run the command again with the change reference formatted as above.".red
152
+ exit 1
153
+ end
154
+ end
155
+ apply_gerrit_patches
156
+ create_and_upload_dev_release
157
+ end
158
+
159
+ usage "cf deploy"
160
+ desc "deploy CloudFoundry system or apply any changes"
161
+ def deploy
162
+ confirm_system
163
+ Dir["#{system}/deployments/*.yml"].each do |deployment|
164
+ set_deployment(deployment)
165
+ bosh_cmd "deploy"
166
+ end
167
+ end
168
+
169
+ usage "cf watch nats"
170
+ desc "subscribe to all nats messages within CloudFoundry"
171
+ def watch_nats
172
+ confirm_system
173
+ nats_props = deployment_manifest("core")["properties"]["nats"]
174
+ user, pass = nats_props["user"], nats_props["password"]
175
+ host, port = nats_props["address"], nats_props["port"]
176
+ nats_uri = "nats://#{user}:#{pass}@#{host}:#{port}"
177
+ sh "nats-sub '*.*' -s #{nats_uri}"
178
+ end
179
+
180
+ usage "cf show password"
181
+ desc "displays the common password for internal access"
182
+ def show_password
183
+ confirm_system
184
+ say system_config.common_password
185
+ end
186
+
187
+ # Creates initial system folder & targets that system folder
188
+ # The +system_config+ configuration does not work until
189
+ # a system folder is created and targeted so that a
190
+ # local configuration manifest can be stored (SystemConfig)
191
+ def setup_system_dir(name)
192
+ system_dir = File.join(base_systems_dir, name)
193
+ unless File.directory?(system_dir)
194
+ say "Creating new system #{name} directory"
195
+ mkdir_p(system_dir)
196
+ end
197
+ set_system(name)
198
+ end
199
+
200
+ # Set +system+ to specified name
201
+ def set_system(name)
202
+ system_dir = File.join(base_systems_dir, name)
203
+ unless File.directory?(system_dir)
204
+ err "CloudFoundry system path '#{system_dir.red}` does not exist"
205
+ end
206
+
207
+ say "CloudFoundry system set to #{system_dir.green}"
208
+ common_config.target_system = system_dir
209
+ common_config.save
210
+ end
211
+
212
+ def target_core_deployment_manifest
213
+ if deployment = Dir["#{system}/deployments/*-core.yml"].first
214
+ set_deployment(deployment)
215
+ end
216
+ end
217
+
218
+ # Helper to tell the CLI to target a specific deployment manifest for the "bosh deploy" command
219
+ def set_deployment(path)
220
+ cmd = Bosh::Cli::Command::Deployment.new
221
+ cmd.set_current(path)
222
+ end
223
+
224
+ def confirm_bosh_target
225
+ return true if skip_validations?
226
+ if bosh_target && bosh_target_uuid
227
+ say("Current BOSH is '#{bosh_target.green}'")
228
+ else
229
+ err("BOSH target not set")
230
+ end
231
+ end
232
+
233
+ def confirm_system
234
+ if system
235
+ say("Current CloudFoundry system is '#{system.green}'")
236
+ else
237
+ err("CloudFoundry system not set")
238
+ end
239
+ end
240
+
241
+ # @return [String] label for the CPI being used by the target BOSH
242
+ # * "aws" - AWS
243
+ #
244
+ # Yet to be supported by bosh-cloudfoundry:
245
+ # * "openstack" - VMWare vSphere
246
+ # * "vsphere" - VMWare vSphere
247
+ # * "vcloud" - VMWare vCloud
248
+ def bosh_provider
249
+ if aws?
250
+ "aws"
251
+ else
252
+ err("Please implement cf.rb's bosh_provider for this IaaS")
253
+ end
254
+ end
255
+
256
+ # Deploying CloudFoundry to AWS?
257
+ # Is the target BOSH's IaaS using the AWS CPI?
258
+ # FIXME Currently only AWS is supported so its always AWS
259
+ def aws?
260
+ true
261
+ end
262
+
263
+ # User is prompted for common values at the
264
+ # start of a command rather than intermittently
265
+ # during a long-running command.
266
+ def confirm_or_prompt_all_defaults
267
+ confirm_bosh_target
268
+ cf_release_dir
269
+ stemcells_dir
270
+ base_systems_dir
271
+ end
272
+
273
+ # Assert that system configuration is available or prompt for values
274
+ def confirm_or_prompt_for_system_requirements
275
+ generate_generatable_options
276
+ validate_root_dns_maps_to_core_ip
277
+ ensure_security_group_prepared
278
+ validate_compute_flavor(core_server_flavor)
279
+ admin_emails
280
+ confirm_or_upload_release
281
+ confirm_or_upload_stemcell
282
+ end
283
+
284
+ # Confirms that the requested release name is
285
+ # already uploaded to BOSH, else
286
+ # proceeds to upload the release
287
+ def confirm_or_upload_release
288
+ switch_to_development_release if options.delete(:edge) || options.delete(:custom) || options.delete(:dev)
289
+ say "Using BOSH release name #{release_name_version} (#{effective_release_version})".green
290
+ unless bosh_release_names.include?(release_name)
291
+ say "BOSH does not contain release #{release_name.green}, uploading...".yellow
292
+ upload_release
293
+ end
294
+ end
295
+
296
+ # Confirms that a stemcell has been uploaded
297
+ # and if so, determines its name/version.
298
+ # Otherwise, uploads the latest stable
299
+ # stemcell.
300
+ #
301
+ # At a more granular level:
302
+ # Are there any stemcells uploaded?
303
+ # If no, then upload one then set stemcell_version
304
+ # If there are stemcells
305
+ # If stemcell_version is set and its not in stemcell list
306
+ # then change stemcell_version to the latest stemcell
307
+ # Else if stemcell_version not set, then set to latest stemcell
308
+ def confirm_or_upload_stemcell
309
+ if stemcell_version
310
+ unless bosh_stemcell_versions.include?(stemcell_version)
311
+ say "Stemcell #{stemcell_name} #{stemcell_version} no longer exists on BOSH, choosing another..."
312
+ system_config.stemcell_version = nil
313
+ else
314
+ say "Using stemcell #{stemcell_name} #{stemcell_version}".green
315
+ return
316
+ end
317
+ end
318
+ unless latest_bosh_stemcell_version
319
+ if stemcell_name == DEFAULT_STEMCELL_NAME
320
+ say "Attempting to upload stemcell #{stemcell_name}..."
321
+ upload_stemcell
322
+ else
323
+ say "Please first upload stemcell #{stemcell_name} or change to default stemcell #{DEFAULT_STEMCELL_NAME}"
324
+ exit 1
325
+ end
326
+ end
327
+ unless stemcell_version && stemcell_version.size
328
+ system_config.stemcell_version = latest_bosh_stemcell_version
329
+ system_config.save
330
+ end
331
+ unless bosh_stemcell_versions.include?(stemcell_version)
332
+ say "Requested stemcell version #{stemcell_version} is not available.".yellow
333
+ system_config.stemcell_version = latest_bosh_stemcell_version
334
+ system_config.save
335
+ end
336
+ say "Using stemcell #{stemcell_name} #{stemcell_version}".green
337
+ end
338
+
339
+
340
+ def confirm_release_name
341
+ return true if skip_validations?
342
+ if release_name = options[:cf_release] || system_config.release_name
343
+ unless bosh_release_names.include?(release_name)
344
+ err("BOSH target #{bosh_target} does not have a release '#{release_name.red}'")
345
+ end
346
+ release_name
347
+ else
348
+ false
349
+ end
350
+ end
351
+
352
+ # Largest version number BOSH stemcell ("bosh-stemcell")
353
+ # @return [String] version number, e.g. "0.6.7"
354
+ def latest_bosh_stemcell_version
355
+ @latest_bosh_stemcell_version ||= begin
356
+ if bosh_stemcell_versions.size > 0
357
+ say "Available BOSH stemcells '#{stemcell_name}': #{bosh_stemcell_versions.join(', ')}"
358
+ bosh_stemcell_versions.last
359
+ else
360
+ say "No stemcells '#{stemcell_name}' uploaded yet"
361
+ nil
362
+ end
363
+ end
364
+ end
365
+
366
+ # Creates/downloads a stemcell; then uploads it to target BOSH
367
+ # If +stemcell_type+ is "stable", then download the latest stemcell tagged "stable"
368
+ # If +stemcell_type+ is "latest", then download the latest stemcell, might not be "stable"
369
+ # If +stemcell_type+ is "custom", then create the stemcell from BOSH source
370
+ def create_or_download_stemcell_then_upload(stemcell_type)
371
+ confirm_bosh_target # fails if CLI is not targeting a BOSH
372
+ if stemcell_type.to_s == "custom"
373
+ create_custom_stemcell
374
+ validate_stemcell_created_successfully
375
+ stemcell_path = move_and_return_created_stemcell
376
+ else
377
+ stemcell_name = bosh_stemcell_name(stemcell_type)
378
+ stemcell_path = download_stemcell(stemcell_name)
379
+ end
380
+ upload_stemcell_to_bosh(stemcell_path)
381
+ end
382
+
383
+ # Creates a custom stemcell and copies it into +stemcells_dir+
384
+ # @return [String] path to the new stemcell file
385
+ def create_custom_stemcell
386
+ if generated_stemcell
387
+ say "Skipping stemcell creation as one sits in the tmp folder waiting patiently..."
388
+ else
389
+ say "Creating new stemcell for '#{bosh_provider.green}'..."
390
+ chdir(repos_dir) do
391
+ clone_or_update_repository("bosh", bosh_git_repo)
392
+ chdir("bosh") do
393
+ sh "bundle install --without development test"
394
+ sh "sudo bundle exec rake stemcell:basic['#{bosh_provider}']"
395
+ sh "sudo chown -R vcap:vcap /var/tmp/bosh/agent-*"
396
+ end
397
+ end
398
+ end
399
+ end
400
+
401
+ def generated_stemcell
402
+ @generated_stemcell ||= Dir['/var/tmp/bosh/agent-*/work/work/*.tgz'].first
403
+ end
404
+
405
+ def validate_stemcell_created_successfully
406
+ err "Stemcell was not created successfully" unless generated_stemcell
407
+ end
408
+
409
+ # Locates the newly created stemcell, moves it into +stemcells_dir+
410
+ # and returns the path of its final resting place
411
+ # @return [String] path to new stemcell file; or nil if no stemcell found
412
+ def move_and_return_created_stemcell
413
+ mv generated_stemcell, "#{stemcells_dir}/"
414
+ File.join(stemcells_dir, File.basename(generated_stemcell))
415
+ end
416
+
417
+ def clone_or_update_repository(name, repo_uri)
418
+ if File.directory?(name)
419
+ chdir(name) do
420
+ say "Updating #{name} repositry..."
421
+ sh "git pull origin master"
422
+ end
423
+ else
424
+ say "Cloning #{repo_uri} repositry..."
425
+ sh "git clone #{repo_uri} #{name}"
426
+ end
427
+ end
428
+
429
+ # The latest relevant public stemcell name
430
+ # Runs 'bosh public stemcells' and parses the output. Currently expects the output
431
+ # to look like:
432
+ # +-----------------------------------------+------------------------+
433
+ # | Name | Tags |
434
+ # +-----------------------------------------+------------------------+
435
+ # | bosh-stemcell-0.5.2.tgz | vsphere |
436
+ # | bosh-stemcell-aws-0.6.4.tgz | aws, stable |
437
+ # | bosh-stemcell-aws-0.6.7.tgz | aws |
438
+ def bosh_stemcell_name(stemcell_type)
439
+ tags = [bosh_provider]
440
+ tags << "stable" if stemcell_type == "stable"
441
+ bosh_stemcells_cmd = "bosh public stemcells --tags #{tags.join(',')}"
442
+ say "Locating bosh stemcell, running '#{bosh_stemcells_cmd}'..."
443
+ `#{bosh_stemcells_cmd} | grep ' bosh-stemcell-' | awk '{ print $2 }' | sort -r | head -n 1`.strip
444
+ end
445
+
446
+ def download_stemcell(stemcell_name)
447
+ mkdir_p(stemcells_dir)
448
+ chdir(stemcells_dir) do
449
+ if File.exists?(stemcell_name)
450
+ say "Stemcell #{stemcell_name} already downloaded".yellow
451
+ else
452
+ say "Downloading public stemcell #{stemcell_name}..."
453
+ bosh_cmd("download public stemcell #{stemcell_name}")
454
+ end
455
+ end
456
+ File.join(stemcells_dir, stemcell_name)
457
+ end
458
+
459
+ def upload_stemcell_to_bosh(stemcell_path)
460
+ say "Uploading stemcell located at #{stemcell_path}..."
461
+ bosh_cmd("upload stemcell #{stemcell_path}")
462
+ @bosh_stemcell_versions = nil # reset cache
463
+ end
464
+
465
+ # It is assumed that there is only one m
466
+ def validate_root_dns_maps_to_core_ip
467
+ core_ip # prompts if not already known
468
+ root_dns # prompts if not already known
469
+
470
+ validate_dns_a_record("api.#{root_dns}", core_ip)
471
+ validate_dns_a_record("demoapp.#{root_dns}", core_ip)
472
+ end
473
+
474
+ # Ensures that the security group exists
475
+ # and has the correct ports open
476
+ def ensure_security_group_prepared
477
+ provider.create_security_group(system_config.security_group, required_public_ports)
478
+ end
479
+
480
+ # TODO this could change based on jobs being included
481
+ def required_public_ports
482
+ {
483
+ ssh: 22,
484
+ http: 80,
485
+ https: 433,
486
+ postgres: 2544,
487
+ resque: 3456,
488
+ nats: 4222,
489
+ router: 8080,
490
+ # TODO serialization_data_server: 8090, - if NFS enabled
491
+ uaa: 8100
492
+ }
493
+ end
494
+
495
+ # Validates that +domain+ is an A record that resolves to +expected_ip_addresses+
496
+ # and no other IP addresses.
497
+ # * +expected_ip_addresses+ is a String (IPv4 address)
498
+ def validate_dns_a_record(domain, expected_ip_address)
499
+ return true if skip_validations?
500
+ say "Checking that DNS #{domain.green} resolves to IP address #{expected_ip_address.green}... ", " "
501
+ packet = Net::DNS::Resolver.start(domain, Net::DNS::A)
502
+ resolved_a_records = packet.answer.map(&:value)
503
+ if packet.answer.size == 0
504
+ error = "Domain '#{domain.green}' does not resolve to an IP address"
505
+ end
506
+ unless resolved_a_records == [expected_ip_address]
507
+ error = "Domain #{domain} should resolve to IP address #{expected_ip_address}"
508
+ end
509
+ if error
510
+ say "ooh no!".red
511
+ say "Please setup your DNS:"
512
+ say "Subdomain: * " + "(wildcard)".yellow
513
+ say "IP address: #{expected_ip_address}"
514
+ err(error)
515
+ else
516
+ say "ok".green
517
+ true
518
+ end
519
+ end
520
+
521
+ # Validates +server_size+ against the known list of instance types/server sizes
522
+ # for the target IaaS.
523
+ #
524
+ # For example, "m1.small" is a valid server size/instance type on all AWS regions
525
+ def validate_compute_flavor(flavor)
526
+ return true if skip_validations?
527
+ if aws?
528
+ unless aws_compute_flavors.select { |flavor| flavor[:id] == flavor }
529
+ err("Server flavor '#{flavor}' is not a valid AWS compute flavor")
530
+ end
531
+ else
532
+ err("Please implemenet cf.rb's validate_compute_flavor for this IaaS")
533
+ end
534
+ end
535
+
536
+ # If any system_config values that are needed are not provided,
537
+ # then ensure that a generated value is stored
538
+ def generate_generatable_options
539
+ common_password
540
+ if aws?
541
+ security_group
542
+ end
543
+ end
544
+
545
+ # Renders the +SystemConfig+ model (+system_config+) into the system's
546
+ # deployment manifest(s).
547
+ def render_system
548
+ renderer = Bosh::CloudFoundry::SystemDeploymentManifestRenderer.new(
549
+ system_config, common_config, config)
550
+ renderer.perform
551
+ end
552
+
553
+ def generate_dea_servers(server_count, server_flavor)
554
+ director_uuid = "DIRECTOR_UUID"
555
+ release_name = "appcloud"
556
+ stemcell_version = "0.6.4"
557
+ if aws?
558
+ resource_pool_cloud_properties = "instance_type: #{server_flavor}"
559
+ else
560
+ err("Please implemenet cf.rb's generate_dea_servers for this IaaS")
561
+ end
562
+ dea_max_memory = 2048 # FIXME a value based on server flavor RAM?
563
+ nats_password = "mynats1234"
564
+ system_dir = File.join(base_systems_dir, system_name)
565
+ mkdir_p(system_dir)
566
+ chdir system_dir do
567
+ require 'bosh-cloudfoundry/generators/dea_generator'
568
+ Bosh::CloudFoundry::Generators::DeaGenerator.start([
569
+ system_name,
570
+ server_count, server_flavor,
571
+ director_uuid, release_name, stemcell_version,
572
+ resource_pool_cloud_properties,
573
+ dea_max_memory,
574
+ nats_password])
575
+ end
576
+ end
577
+
578
+ # Valdiate that +service_name+ is a known, supported service name
579
+ def validate_service_name(service_name)
580
+ return true if skip_validations?
581
+ unless supported_services.include?(service_name)
582
+ supported_services_list = supported_services.join(", ")
583
+ err("Service '#{service_name}' is not a supported service, such as #{supported_services_list}")
584
+ end
585
+ end
586
+
587
+ def supported_services
588
+ %w[postgresql redis]
589
+ end
590
+
591
+ def service_config(service_name)
592
+ case service_name.to_sym
593
+ when :postgresql
594
+ Bosh::CloudFoundry::Config::PostgresqlServiceConfig.build_from_system_config(system_config)
595
+ when :redis
596
+ Bosh::CloudFoundry::Config::RedisServiceConfig.build_from_system_config(system_config)
597
+ else
598
+ raise "please add #{service_name} support to #service_config method"
599
+ end
600
+ end
601
+
602
+ def generate_service_servers(service_name, server_count, server_flavor)
603
+ director_uuid = "DIRECTOR_UUID"
604
+ release_name = "appcloud"
605
+ stemcell_version = "0.6.4"
606
+ if aws?
607
+ resource_pool_cloud_properties = "instance_type: #{server_flavor}"
608
+ else
609
+ err("Please implemenet cf.rb's generate_service_servers for this IaaS")
610
+ end
611
+ persistent_disk = 16192
612
+ nats_password = "mynats1234"
613
+ system_dir = File.join(base_systems_dir, system_name)
614
+ mkdir_p(system_dir)
615
+ chdir system_dir do
616
+ require 'bosh-cloudfoundry/generators/service_generator'
617
+ Bosh::CloudFoundry::Generators::ServiceGenerator.start([
618
+ system_name,
619
+ service_name, server_count, server_flavor,
620
+ director_uuid, release_name, stemcell_version,
621
+ resource_pool_cloud_properties, persistent_disk,
622
+ nats_password])
623
+ end
624
+ end
625
+
626
+ def default_core_server_flavor
627
+ if aws?
628
+ "m1.large"
629
+ else
630
+ err("Please implement cf.rb's default_core_server_flavor for this IaaS")
631
+ end
632
+ end
633
+
634
+ def default_dea_server_flavor
635
+ if aws?
636
+ "m1.large"
637
+ else
638
+ err("Please implement cf.rb's default_server_flavor for this IaaS")
639
+ end
640
+ end
641
+
642
+ def default_service_server_flavor(service_name)
643
+ if aws?
644
+ "m1.xlarge"
645
+ else
646
+ err("Please implement cf.rb's default_service_server_flavor for this IaaS")
647
+ end
648
+ end
649
+
650
+ # @return [Array] of [Hash] for each supported compute flavor
651
+ # Example [Hash] { :bits => 0, :cores => 2, :disk => 0,
652
+ # :id => 't1.micro', :name => 'Micro Instance', :ram => 613}
653
+ def aws_compute_flavors
654
+ Fog::Compute::AWS::FLAVORS
655
+ end
656
+
657
+ # a helper object for the target BOSH provider
658
+ def provider
659
+ @provider ||= Bosh::CloudFoundry::Providers.for_bosh_provider_name(system_config)
660
+ end
661
+
662
+ def bosh_cmd(command)
663
+ full_command = "bosh -n --color #{command}"
664
+ sh full_command
665
+ end
666
+
667
+ end
668
+ end