inception 0.1.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 (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