inception 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.chef/knife.rb +4 -0
  2. data/.gitignore +20 -0
  3. data/.kitchen.yml +42 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +23 -0
  6. data/Berksfile +8 -0
  7. data/Berksfile.lock +9 -0
  8. data/Gemfile +18 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +139 -0
  11. data/Rakefile +66 -0
  12. data/TODO.md +26 -0
  13. data/bin/bosh-inception +8 -0
  14. data/config/ssh/kitchen-aws +23 -0
  15. data/cookbooks/bosh_inception/README.md +15 -0
  16. data/cookbooks/bosh_inception/attributes/default.rb +20 -0
  17. data/cookbooks/bosh_inception/files/default/Gemfile.cf +4 -0
  18. data/cookbooks/bosh_inception/files/default/Gemfile.micro +5 -0
  19. data/cookbooks/bosh_inception/metadata.rb +32 -0
  20. data/cookbooks/bosh_inception/recipes/default.rb +15 -0
  21. data/cookbooks/bosh_inception/recipes/install_bosh.rb +37 -0
  22. data/cookbooks/bosh_inception/recipes/install_ruby.rb +10 -0
  23. data/cookbooks/bosh_inception/recipes/mount_store_volume.rb +24 -0
  24. data/cookbooks/bosh_inception/recipes/packages.rb +23 -0
  25. data/cookbooks/bosh_inception/recipes/setup_git.rb +34 -0
  26. data/cookbooks/bosh_inception/recipes/useful_dirs.rb +13 -0
  27. data/inception.gemspec +42 -0
  28. data/lib/bosh/providers.rb +41 -0
  29. data/lib/bosh/providers/README.md +5 -0
  30. data/lib/bosh/providers/cli/aws_provider_cli.rb +58 -0
  31. data/lib/bosh/providers/cli/openstack_provider_cli.rb +47 -0
  32. data/lib/bosh/providers/cli/provider_cli.rb +17 -0
  33. data/lib/bosh/providers/clients/aws_provider_client.rb +168 -0
  34. data/lib/bosh/providers/clients/fog_provider_client.rb +161 -0
  35. data/lib/bosh/providers/clients/openstack_provider_client.rb +65 -0
  36. data/lib/bosh/providers/constants/aws_constants.rb +25 -0
  37. data/lib/bosh/providers/constants/openstack_constants.rb +12 -0
  38. data/lib/inception.rb +9 -0
  39. data/lib/inception/cli.rb +136 -0
  40. data/lib/inception/cli_helpers/display.rb +26 -0
  41. data/lib/inception/cli_helpers/infrastructure.rb +157 -0
  42. data/lib/inception/cli_helpers/interactions.rb +15 -0
  43. data/lib/inception/cli_helpers/prepare_deploy_settings.rb +89 -0
  44. data/lib/inception/cli_helpers/provider.rb +14 -0
  45. data/lib/inception/cli_helpers/settings.rb +47 -0
  46. data/lib/inception/inception_server.rb +305 -0
  47. data/lib/inception/inception_server_cookbook.rb +89 -0
  48. data/lib/inception/next_deploy_actions.rb +20 -0
  49. data/lib/inception/version.rb +3 -0
  50. data/nodes/.gitkeep +0 -0
  51. data/spec/assets/.gitkeep +0 -0
  52. data/spec/assets/gitconfig +5 -0
  53. data/spec/assets/settings/aws-before-server.yml +14 -0
  54. data/spec/assets/settings/aws-created-server.yml +31 -0
  55. data/spec/integration/.gitkeep +0 -0
  56. data/spec/integration/aws/aws_basic_spec.rb +39 -0
  57. data/spec/spec_helper.rb +50 -0
  58. data/spec/support/aws/aws_helpers.rb +73 -0
  59. data/spec/support/settings_helper.rb +20 -0
  60. data/spec/support/stdout_capture.rb +17 -0
  61. data/spec/unit/.gitkeep +0 -0
  62. data/spec/unit/bosh/providers/aws_spec.rb +199 -0
  63. data/spec/unit/cli_delete_spec.rb +39 -0
  64. data/spec/unit/cli_deploy_aws_spec.rb +84 -0
  65. data/spec/unit/cli_ssh_spec.rb +82 -0
  66. data/spec/unit/inception_server_cookbook_spec.rb +61 -0
  67. data/spec/unit/inception_server_spec.rb +58 -0
  68. data/test/integration/default/bats/discover_user.bash +2 -0
  69. data/test/integration/default/bats/install_ruby.bats +8 -0
  70. data/test/integration/default/bats/useful_dirs.bats +8 -0
  71. data/test/integration/default/bats/user.bats +9 -0
  72. data/test/integration/default/bats/verify_bosh.bats +13 -0
  73. data/test/integration/default/bats/verify_git.bats +18 -0
  74. metadata +342 -0
@@ -0,0 +1,26 @@
1
+ module Inception::CliHelpers
2
+ module Display
3
+ # Display header for a new section of the bootstrapper
4
+ def header(title, options={})
5
+ say "" # golden whitespace
6
+ if skipping = options[:skipping]
7
+ say "Skipping #{title}", [:yellow, :bold]
8
+ say skipping
9
+ else
10
+ say title, [:green, :bold]
11
+ end
12
+ say "" # more golden whitespace
13
+ end
14
+
15
+ def error(message)
16
+ say message, :red
17
+ exit 1
18
+ end
19
+
20
+ def confirm(message)
21
+ say "Confirming: #{message}", green
22
+ say "" # bonus golden whitespace
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,157 @@
1
+ module Inception::CliHelpers
2
+ module Infrastructure
3
+ # Prompts user to choose an Iaas provider
4
+ # Sets settings.provider.name
5
+ def configure_provider
6
+ unless valid_infrastructure?
7
+ choose_fog_provider unless settings.exists?("provider.name")
8
+ choose_provider unless settings.exists?("provider.name")
9
+ setup_provider_credentials
10
+ end
11
+ confirm_infrastructure
12
+ end
13
+
14
+ # Displays a prompt for known IaaS that are configured
15
+ # within .fog config file if found.
16
+ #
17
+ # If no ~/.fog file found or user chooses "Alternate credentials"
18
+ # then no changes are made to settings.
19
+ #
20
+ # For example:
21
+ #
22
+ # 1. AWS (default)
23
+ # 2. AWS (bosh)
24
+ # 3. Alternate credentials
25
+ # Choose infrastructure: 1
26
+ #
27
+ # If .fog config only contains one provider, do not prompt.
28
+ #
29
+ # fog config file looks like:
30
+ # :default:
31
+ # :aws_access_key_id: PERSONAL_ACCESS_KEY
32
+ # :aws_secret_access_key: PERSONAL_SECRET
33
+ # :bosh:
34
+ # :aws_access_key_id: SPECIAL_IAM_ACCESS_KEY
35
+ # :aws_secret_access_key: SPECIAL_IAM_SECRET_KEY
36
+ #
37
+ # Convert this into:
38
+ # { "AWS (default)" => {:aws_access_key_id => ...}, "AWS (bosh)" => {...} }
39
+ #
40
+ # Then display options to user to choose.
41
+ #
42
+ # Currently detects following fog providers:
43
+ # * AWS
44
+ # * OpenStack
45
+ #
46
+ # If "Alternate credentials" is selected, then user is prompted for fog
47
+ # credentials:
48
+ # * provider?
49
+ # * access keys?
50
+ # * API URI or region?
51
+ #
52
+ # Sets (unless 'Alternate credentials' is chosen)
53
+ # * settings.provider.name
54
+ # * settings.provider.credentials
55
+ #
56
+ # For AWS, the latter has keys:
57
+ # {:aws_access_key_id, :aws_secret_access_key}
58
+ #
59
+ # For OpenStack, the latter has keys:
60
+ # {:openstack_username, :openstack_api_key, :openstack_tenant
61
+ # :openstack_auth_url, :openstack_region }
62
+ def choose_fog_provider
63
+ fog_providers = {}
64
+ # Prepare menu options:
65
+ # each provider/profile name gets a menu choice option
66
+ fog_config.inject({}) do |iaas_options, fog_profile|
67
+ profile_name, profile = fog_profile
68
+ if profile[:aws_access_key_id]
69
+ # TODO does fog have inbuilt detection algorithm?
70
+ fog_providers["AWS (#{profile_name})"] = {
71
+ "name" => "aws",
72
+ "provider" => "AWS",
73
+ "aws_access_key_id" => profile[:aws_access_key_id],
74
+ "aws_secret_access_key" => profile[:aws_secret_access_key]
75
+ }
76
+ end
77
+ if profile[:openstack_username]
78
+ # TODO does fog have inbuilt detection algorithm?
79
+ fog_providers["OpenStack (#{profile_name})"] = {
80
+ "name" => "openstack",
81
+ "provider" => "OpenStack",
82
+ "openstack_username" => profile[:openstack_username],
83
+ "openstack_api_key" => profile[:openstack_api_key],
84
+ "openstack_tenant" => profile[:openstack_tenant],
85
+ "openstack_auth_url" => profile[:openstack_auth_url],
86
+ "openstack_region" => profile[:openstack_region]
87
+ }
88
+ end
89
+ end
90
+ # Display menu
91
+ # Include "Alternate credentials" as the last option
92
+ if fog_providers.keys.size > 0
93
+ hl.choose do |menu|
94
+ menu.prompt = "Choose infrastructure: "
95
+ fog_providers.each do |label, credentials|
96
+ menu.choice(label) do
97
+ settings.set("provider.name", credentials.delete("name"))
98
+ settings.set("provider.credentials", credentials)
99
+ save_settings!
100
+ end
101
+ end
102
+ menu.choice("Alternate credentials")
103
+ end
104
+ end
105
+ end
106
+
107
+ # Prompts user to pick from the supported regions
108
+ def choose_provider
109
+ hl.choose do |menu|
110
+ menu.prompt = "Choose infrastructure: "
111
+ menu.choice("AWS") do
112
+ settings.provider["name"] = "aws"
113
+ end
114
+ menu.choice("OpenStack") do
115
+ settings.provider["name"] = "openstack"
116
+ end
117
+ end
118
+ end
119
+
120
+ def setup_provider_credentials
121
+ say "Using provider #{settings.provider.name}:"
122
+ say ""
123
+ settings.set_default("provider", {}) # to ensure settings.provider exists
124
+ provider_cli = Bosh::Providers.provider_cli(settings.provider.name, settings.provider)
125
+ provider_cli.perform
126
+ settings["provider"] = provider_cli.export_attributes
127
+ settings.create_accessors!
128
+ end
129
+
130
+ def valid_infrastructure?
131
+ settings.exists?("provider.name") &&
132
+ settings.exists?("provider.region") &&
133
+ settings.exists?("provider.credentials") &&
134
+ provider_client
135
+ end
136
+
137
+ def confirm_infrastructure
138
+ confirm "Using #{settings.provider.name}/#{settings.provider.region}"
139
+ end
140
+
141
+ def fog_config
142
+ @fog_config ||= begin
143
+ if File.exists?(File.expand_path(fog_config_path))
144
+ say "Found infrastructure API credentials at #{fog_config_path} (override with $FOG)"
145
+ YAML.load_file(File.expand_path(fog_config_path))
146
+ else
147
+ say "No existing #{fog_config_path} fog configuration file", :yellow
148
+ {}
149
+ end
150
+ end
151
+ end
152
+
153
+ def fog_config_path
154
+ ENV['FOG'] || "~/.fog"
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,15 @@
1
+ module Inception::CliHelpers
2
+ module Interactions
3
+ def cyan; "\033[36m" end
4
+ def clear; "\033[0m" end
5
+ def bold; "\033[1m" end
6
+ def red; "\033[31m" end
7
+ def green; "\033[32m" end
8
+ def yellow; "\033[33m" end
9
+
10
+ # Helper to access HighLine for ask & menu prompts
11
+ def hl
12
+ @hl ||= HighLine.new
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,89 @@
1
+ module Inception::CliHelpers
2
+ module PrepareDeploySettings
3
+ def update_git_config
4
+ gitconfig = File.expand_path("~/.gitconfig")
5
+ if File.exists?(gitconfig)
6
+ say "Using your git user.name (#{`git config -f #{gitconfig} user.name`.strip})"
7
+ settings.set("git.name", `git config -f #{gitconfig} user.name`.strip)
8
+ settings.set("git.email", `git config -f #{gitconfig} user.email`.strip)
9
+ save_settings!
10
+ end
11
+ end
12
+
13
+ # Attempt to provision a new public IP; if none available,
14
+ # then look for a pre-provisioned public IP that's not assigned
15
+ # to a server; else error. The user needs to go get more
16
+ # public IP addresses in this region.
17
+ def provision_or_reuse_public_ip_address_for_inception
18
+ say "Acquiring a public IP address... "
19
+ if public_ip = provider_client.provision_or_reuse_public_ip_address
20
+ say public_ip, :green
21
+ settings.set("inception.provisioned.ip_address", public_ip)
22
+ save_settings!
23
+ else
24
+ say "none available.", :red
25
+ error "Please rustle up at least one public IP address and try again."
26
+ end
27
+ end
28
+
29
+ def default_server_name
30
+ "inception"
31
+ end
32
+
33
+ def default_key_pair_name
34
+ default_server_name
35
+ end
36
+
37
+ def recreate_key_pair_for_inception
38
+ key_pair_name = settings.set_default("inception.key_pair.name", default_key_pair_name)
39
+ provider_client.delete_key_pair_if_exists(key_pair_name)
40
+ key_pair = provider_client.create_key_pair(key_pair_name)
41
+ settings.set("inception.key_pair.private_key", key_pair.private_key)
42
+ settings.set("inception.key_pair.fingerprint", key_pair.fingerprint)
43
+ save_settings!
44
+ end
45
+
46
+ def private_key_path_for_inception
47
+ @private_key_path_for_inception ||= File.join(settings_dir, "ssh", settings.inception.key_pair.name)
48
+ end
49
+
50
+ # The keys for the inception server originate from the provider and are cached in
51
+ # the manifest. The private key is stored locally; the public key is placed
52
+ # on the inception server.
53
+ def recreate_private_key_file_for_inception
54
+ mkdir_p(File.dirname(private_key_path_for_inception))
55
+ File.chmod(0700, File.dirname(private_key_path_for_inception))
56
+ File.open(private_key_path_for_inception, "w") { |file| file << settings.inception.key_pair.private_key }
57
+ File.chmod(0600, private_key_path_for_inception)
58
+ end
59
+
60
+
61
+ # Required settings:
62
+ # * git.name
63
+ # * git.email
64
+ def validate_deploy_settings
65
+ begin
66
+ settings.git.name
67
+ settings.git.email
68
+ rescue Settingslogic::MissingSetting => e
69
+ error "Please setup local git user.name & user.email config; or specify git.name & git.email in settings.yml"
70
+ end
71
+
72
+ begin
73
+ settings.provider.name
74
+ settings.provider.region
75
+ settings.provider.credentials
76
+ rescue Settingslogic::MissingSetting => e
77
+ error "Wooh there, we need provider.name, provider.region, provider.credentials in settings.yml to proceed."
78
+ end
79
+
80
+ begin
81
+ settings.inception.provisioned.ip_address
82
+ settings.inception.key_pair.name
83
+ settings.inception.key_pair.private_key
84
+ rescue Settingslogic::MissingSetting => e
85
+ error "Wooh there, we need inception.provisioned.ip_address, inception.key_pair.name, & inception.key_pair.private_key in settings.yml to proceed."
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,14 @@
1
+ module Inception::CliHelpers
2
+ module Provider
3
+ def provider_client
4
+ @provider_client ||= begin
5
+ Bosh::Providers.provider_client(settings.provider)
6
+ end
7
+ end
8
+
9
+ # If the +provider_client+ uses fog, then this will return its +fog_compute+ client object
10
+ def fog_compute
11
+ provider_client.respond_to?(:fog_compute) ? provider_client.fog_compute : nil
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,47 @@
1
+ require "settingslogic"
2
+
3
+ module Inception::CliHelpers
4
+ module Settings
5
+ include FileUtils
6
+
7
+ # The base directory for holding the manifest settings file
8
+ # and private keys
9
+ #
10
+ # Defaults to ~/.bosh_inception; and can be overridden with either:
11
+ # * $SETTINGS - to a folder (supported method)
12
+ def settings_dir
13
+ @settings_dir ||= File.expand_path(ENV["SETTINGS"] || "~/.bosh_inception")
14
+ end
15
+
16
+ def settings_ssh_dir
17
+ File.join(settings_dir, "ssh")
18
+ end
19
+
20
+ def settings_path
21
+ @settings_path ||= File.join(settings_dir, "settings.yml")
22
+ end
23
+
24
+ def settings
25
+ @settings ||= begin
26
+ unless File.exists?(settings_path)
27
+ mkdir_p(settings_ssh_dir)
28
+ File.open(settings_path, "w") { |file| file << "--- {}" }
29
+ end
30
+ chmod(0600, settings_path)
31
+ chmod(0700, settings_ssh_dir)
32
+ Settingslogic.new(settings_path)
33
+ end
34
+ end
35
+
36
+ # Saves current nested Settingslogic into pure Hash-based YAML file
37
+ # Recreates accessors on Settingslogic object (since something has changed)
38
+ def save_settings!
39
+ File.open(settings_path, "w") { |f| f << settings.to_nested_hash.to_yaml }
40
+ settings.create_accessors!
41
+ end
42
+
43
+ def migrate_old_settings
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,305 @@
1
+ require "fog"
2
+
3
+ module Inception
4
+ class InceptionServer
5
+
6
+ DEFAULT_SERVER_NAME = "inception"
7
+ DEFAULT_FLAVOR = "m1.small"
8
+ DEFAULT_DISK_SIZE = 16
9
+ DEFAULT_SECURITY_GROUPS = ["ssh"]
10
+
11
+ attr_reader :attributes
12
+
13
+ # @provider_client [Bosh::Providers::FogProvider] - interact with IaaS
14
+ # @attributes [Settingslogic]
15
+ #
16
+ # Required @attributes:
17
+ # {
18
+ # "name" => "inception",
19
+ # "ip_address" => "54.214.15.178",
20
+ # "key_pair" => {
21
+ # "name" => "inception",
22
+ # "private_key" => "private_key",
23
+ # "public_key" => "public_key"
24
+ # }
25
+ # }
26
+ #
27
+ # Including optional @attributes and default values:
28
+ # {
29
+ # "name" => "inception",
30
+ # "ip_address" => "54.214.15.178",
31
+ # "security_groups" => ["ssh"],
32
+ # "flavor" => "m1.small",
33
+ # "key_pair" => {
34
+ # "name" => "inception",
35
+ # "private_key" => "private_key",
36
+ # "public_key" => "public_key"
37
+ # }
38
+ # }
39
+ def initialize(provider_client, attributes, ssh_dir)
40
+ @provider_client = provider_client
41
+ @ssh_dir = ssh_dir
42
+ @attributes = attributes.is_a?(Hash) ? Settingslogic.new(attributes) : attributes
43
+ raise "@attributes must be Settingslogic (or Hash)" unless @attributes.is_a?(Settingslogic)
44
+ end
45
+
46
+ # Create the underlying server, with key pair & security groups, unless it is already created
47
+ #
48
+ # The @attributes hash is updated with a `provisioned` key during/after creation.
49
+ # When saved as YAML it might look like:
50
+ # inception:
51
+ # provisioned:
52
+ # image_id: ami-123456
53
+ # server_id: i-e7f005d2
54
+ # security_groups:
55
+ # - ssh
56
+ # - mosh
57
+ # username: ubuntu
58
+ # disk_device: /dev/sdi
59
+ # host: ec2-54-214-15-178.us-west-2.compute.amazonaws.com
60
+ # validated: true
61
+ # converged: true
62
+ def create
63
+ validate_attributes_for_bootstrap
64
+ ensure_required_security_groups
65
+ create_missing_default_security_groups
66
+ bootstrap_vm
67
+ attach_persistent_disk
68
+ end
69
+
70
+ # Delete the server, volume and release the IP address
71
+ def delete_all
72
+ delete_server
73
+ delete_volume
74
+ delete_key_pair
75
+ release_ip_address
76
+ end
77
+
78
+ def delete_server
79
+ @fog_server = nil # force reload of fog_server model
80
+ if fog_server
81
+ print "Deleting server..."
82
+ fog_server.destroy
83
+ wait_for_termination(fog_server) unless Fog.mocking?
84
+ puts "done."
85
+ else
86
+ puts "Server already destroyed"
87
+ end
88
+ provisioned.delete("host")
89
+ provisioned.delete("server_id")
90
+ provisioned.delete("username")
91
+ end
92
+
93
+ def delete_volume
94
+ volume_id = provisioned.exists?("disk_device.volume_id")
95
+ if volume_id && (volume = fog_compute.volumes.get(volume_id)) && volume.ready?
96
+ print "Deleting volume..."
97
+ volume.destroy
98
+ wait_for_termination(volume, "deleting")
99
+ puts ""
100
+ else
101
+ puts "Volume already destroyed"
102
+ end
103
+ provisioned.delete("disk_device")
104
+ end
105
+
106
+ def delete_key_pair
107
+ key_pair_name = attributes.exists?("key_pair.name")
108
+ if key_pair_name && key_pair = fog_compute.key_pairs.get(key_pair_name)
109
+ puts "Deleting key pair '#{key_pair_name}'"
110
+ key_pair.destroy
111
+ else
112
+ puts "Keypair already destroyed"
113
+ end
114
+ attributes.delete("key_pair")
115
+ end
116
+
117
+
118
+ def release_ip_address
119
+ public_ip = provisioned.exists?("ip_address")
120
+ if public_ip && ip_address = fog_compute.addresses.get(public_ip)
121
+ puts "Releasing IP address #{public_ip}"
122
+ ip_address.destroy
123
+ else
124
+ puts "IP address already released"
125
+ end
126
+ provisioned.delete("ip_address")
127
+ end
128
+
129
+ def security_groups
130
+ @attributes.security_groups
131
+ end
132
+
133
+ def server_name
134
+ @attributes["name"] ||= DEFAULT_SERVER_NAME
135
+ @attributes.name
136
+ end
137
+
138
+ def key_name
139
+ @attributes.key_pair.name
140
+ end
141
+
142
+ def private_key_path
143
+ @private_key_path ||= File.join(@ssh_dir, key_name)
144
+ end
145
+
146
+ # Flavor/instance type of the server to be provisioned
147
+ # TODO: DEFAULT_FLAVOR should become IaaS/provider specific
148
+ def flavor
149
+ @attributes["flavor"] ||= DEFAULT_FLAVOR
150
+ end
151
+
152
+ # Size of attached persistent disk for the inception server
153
+ def disk_size
154
+ @attributes["disk_size"] ||= DEFAULT_DISK_SIZE
155
+ end
156
+
157
+ def ip_address
158
+ provisioned.ip_address
159
+ end
160
+
161
+ def image_id
162
+ @attributes["image_id"] ||= @provider_client.raring_image_id
163
+ end
164
+
165
+ # The progresive/final attributes of the provisioned Inception server &
166
+ # persistent disk.
167
+ def provisioned
168
+ @attributes["provisioned"] = {} unless @attributes["provisioned"]
169
+ @attributes.provisioned
170
+ end
171
+
172
+ # Because @attributes["provisioned"] is not the same as @attributes.provisioned
173
+ # we need a helper to export the complete nested attributes.
174
+ def export_attributes
175
+ attrs = attributes.to_nested_hash
176
+ attrs["provisioned"] = provisioned.to_nested_hash
177
+ attrs
178
+ end
179
+
180
+ def disk_devices
181
+ provisioned["disk_device"] ||= default_disk_device
182
+ end
183
+
184
+ def external_disk_device
185
+ disk_devices["external"]
186
+ end
187
+
188
+ def default_disk_device
189
+ case @provider_client
190
+ when Bosh::Providers::Clients::AwsProviderClient
191
+ { "external" => "/dev/sdf", "internal" => "/dev/xvdf" }
192
+ when Bosh::Providers::Clients::OpenStackProviderClient
193
+ { "external" => "/dev/vdc", "internal" => "/dev/vdc" }
194
+ else
195
+ raise "Please implement InceptionServer#default_disk_device for #{@provider_client.class}"
196
+ end
197
+ end
198
+
199
+ def user_host
200
+ "#{provisioned.username}@#{provisioned.host}"
201
+ end
202
+
203
+ def fog_server
204
+ @fog_server ||= begin
205
+ if server_id = provisioned["server_id"]
206
+ fog_compute.servers.get(server_id)
207
+ end
208
+ end
209
+ end
210
+
211
+ def fog_compute
212
+ @provider_client.fog_compute
213
+ end
214
+
215
+ protected
216
+ # set_resource_name(fog_server, "inception")
217
+ # set_resource_name(volume, "inception-root")
218
+ # set_resource_name(volume, "inception-store")
219
+ def set_resource_name(resource, name)
220
+ @provider_client.set_resource_name(resource, name)
221
+ end
222
+
223
+ def fog_attributes
224
+ {
225
+ :image_id => image_id,
226
+ :groups => security_groups,
227
+ :key_name => key_name,
228
+ :private_key_path => private_key_path,
229
+ :flavor_id => flavor,
230
+ :public_ip_address => ip_address,
231
+ :bits => 64,
232
+ :username => "ubuntu",
233
+ }
234
+ end
235
+
236
+ def validate_attributes_for_bootstrap
237
+ missing_attributes = []
238
+ missing_attributes << "provisioned.ip_address" unless @attributes.exists?("provisioned.ip_address")
239
+ missing_attributes << "key_pair.private_key" unless @attributes.exists?("key_pair.private_key")
240
+ if missing_attributes.size > 0
241
+ raise "Missing InceptionServer attributes: #{missing_attributes.join(', ')}"
242
+ end
243
+ end
244
+
245
+ # ssh group must be first (bootstrap method looks for port 22 in first group)
246
+ def ensure_required_security_groups
247
+ if @attributes["security_groups"] && @attributes["security_groups"].is_a?(Array)
248
+ unless @attributes["security_groups"].include?("ssh")
249
+ @attributes["security_groups"] = ["ssh", *@attributes["security_groups"]]
250
+ end
251
+ else
252
+ @attributes["security_groups"] = ["ssh"]
253
+ end
254
+ end
255
+
256
+ def create_missing_default_security_groups
257
+ # provider method only creates group if missing
258
+ @provider_client.create_security_group("ssh", "ssh", {ssh: 22})
259
+ end
260
+
261
+ def bootstrap_vm
262
+ unless fog_server
263
+ say "Booting #{flavor} inception server..."
264
+ @fog_server = @provider_client.bootstrap(fog_attributes)
265
+ provisioned["server_id"] = fog_server.id
266
+ provisioned["host"] = fog_server.dns_name || fog_server.public_ip_address
267
+ provisioned["username"] = fog_attributes[:username]
268
+ end
269
+ set_resource_name(fog_server, server_name)
270
+ end
271
+
272
+ def attach_persistent_disk
273
+ unless Fog.mocking?
274
+ Fog.wait_for(60) { fog_server.sshable?(ssh_options) }
275
+ end
276
+
277
+ unless volume = @provider_client.find_server_device(fog_server, external_disk_device)
278
+ say "Provisioning #{disk_size}Gb persistent disk for inception server..."
279
+ volume = @provider_client.create_and_attach_volume("Inception Disk", disk_size, fog_server, external_disk_device)
280
+ disk_devices["volume_id"] = volume.id
281
+ end
282
+ set_resource_name(volume, server_name)
283
+ end
284
+
285
+ def ssh_options
286
+ {
287
+ keys: [private_key_path]
288
+ }
289
+ end
290
+
291
+ # Poll a fog model until it terminates; print . each second
292
+ def wait_for_termination(fog_model, state_to_wait_for="terminated")
293
+ fog_model.wait_for do
294
+ print "."
295
+ state == state_to_wait_for
296
+ end
297
+ end
298
+
299
+ protected
300
+ # TODO emit events rather than writing directly to STDOUT
301
+ def say(*args)
302
+ puts(*args)
303
+ end
304
+ end
305
+ end