bosh-cloudfoundry 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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