cloud-maker 0.3.0 → 0.4.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.
@@ -4,6 +4,17 @@ require 'cloud-maker'
4
4
  class CloudMakerCLI < Thor
5
5
  include Thor::Actions
6
6
 
7
+ CONFIG_PATH = '~/.cloud-maker.yaml'
8
+
9
+ def initialize(*args)
10
+ super
11
+ begin
12
+ @global_options = YAML::load(File.open(File.expand_path(CONFIG_PATH)))
13
+ rescue
14
+ @global_options = {}
15
+ end
16
+ end
17
+
7
18
  map "--version" => :version, "-v" => :version
8
19
 
9
20
  desc "user_data [INSTANCE_CONFIG_YAML]", "Generate the Cloud Init user data for an instance described by INSTANCE_CONFIG_YAML"
@@ -28,6 +39,37 @@ class CloudMakerCLI < Thor
28
39
  say "cloud-maker #{Gem::Specification.find_by_name('cloud-maker').version.version}"
29
40
  end
30
41
 
42
+ desc "authorize_github", "Fetch an API authorization token from github."
43
+ def authorize_github
44
+ say "This will generate an OAuth token for use when fetching #{"github://".cyan} urls. It will be saved in #{CONFIG_PATH.cyan}."
45
+ say "You can remove this authorization from the GitHub interface if needed."
46
+ say "Your password will not be saved to disk.".green
47
+ user = ask("What is your GitHub #{"username".cyan}?")
48
+ password = ask("What is your GitHub #{"password".cyan}?")
49
+ begin
50
+ response = JSON.parse(RestClient::Request.new(
51
+ :method => "POST",
52
+ :url => 'https://api.github.com/authorizations',
53
+ :user => user,
54
+ :password => password,
55
+ :payload => {
56
+ :note => "CloudMaker",
57
+ :note_url => "https://github.com/airbnb/cloud-maker",
58
+ :scopes => ["repo"]
59
+ }.to_json,
60
+ :headers => {
61
+ :content_type => 'application/json'
62
+ }
63
+ ).execute)
64
+ @global_options['github_token'] = response["token"]
65
+ save_options!
66
+ say "Your GitHub credentials have been successfully updated.".green
67
+ rescue Exception => e
68
+ say e.to_s.red
69
+ say "Your GitHub credentials could not be updated.".red
70
+ end
71
+ end
72
+
31
73
  desc "terminate [AWS_INSTANCE_ID]", "Terminate the specified AWS instance"
32
74
  method_option :aws_access_key_id,
33
75
  :desc => "Your AWS access key id",
@@ -167,8 +209,32 @@ class CloudMakerCLI < Thor
167
209
  puts
168
210
  end
169
211
 
212
+ def save_options!
213
+ path = File.expand_path(CONFIG_PATH)
214
+ File.open(path, 'w+') {|f| f.write(@global_options.to_yaml)}
215
+ end
216
+
170
217
  def build_config(instance_config_yaml, options)
171
- config = CloudMaker::Config.from_yaml(instance_config_yaml, 'import_ec2' => true)
218
+ begin
219
+ config = CloudMaker::Config.from_yaml(instance_config_yaml, 'import_ec2' => true, 'github_token' => @global_options['github_token'])
220
+ rescue CloudMaker::Config::GitHubContentNotFound => e
221
+ say e.to_s.red
222
+ if @global_options['github_token']
223
+ say "You do have GitHub OAuth configured, however you can redo the configuration if the current OAuth token is no longer valid.".yellow
224
+ else
225
+ say "You don't have a GitHub OAuth token configured, if this is a private repo doing so is necessary.".yellow
226
+ end
227
+ if yes?("Would you like to configure OAuth and retry now?")
228
+ authorize_github
229
+ retry
230
+ else
231
+ exit 1
232
+ end
233
+ rescue Exception => e
234
+ say e.to_s.red
235
+ exit 1
236
+ end
237
+
172
238
  options.set.each_pair {|key, val| config[key] = val}
173
239
 
174
240
  config['tags'] ||= {}
@@ -1,8 +1,10 @@
1
+ require 'json'
1
2
  require 'yaml'
2
3
  require 'thor'
3
4
  require 'colorize'
4
5
  require 'deep_merge'
5
6
  require 'aws-sdk'
7
+ require 'rest-client'
6
8
 
7
9
  require 'cloud_maker/config'
8
10
  require 'cloud_maker/ec2'
@@ -1,5 +1,19 @@
1
1
  module CloudMaker
2
2
  class Config
3
+ class ContentNotFound < RuntimeError
4
+ attr_accessor :path
5
+ def initialize(message, path)
6
+ super(message)
7
+ self.path = path
8
+ end
9
+ end
10
+ class GitHubContentNotFound < ContentNotFound
11
+ end
12
+ class HTTPContentNotFound < ContentNotFound
13
+ end
14
+ class FileContentNotFound < ContentNotFound
15
+ end
16
+
3
17
  # Public: Gets/Sets the CloudMaker specific properties. The options hash
4
18
  # is formatted as:
5
19
  # {
@@ -84,9 +98,23 @@ module CloudMaker
84
98
  self.cloud_config = cloud_config
85
99
 
86
100
  self.import(self.class.new(EC2::CLOUD_MAKER_CONFIG)) if (extra_options['import_ec2'])
101
+
102
+ # It's important here that reverse duplicates the imports array as executing the import will
103
+ # add the imported configs imports to the list and we do NOT want to reimport those as well.
87
104
  self.imports.reverse.each do |import_path|
88
- self.import(self.class.from_yaml(import_path))
105
+ self.import(self.class.from_yaml(import_path, self.extra_options))
89
106
  end
107
+ self['tags'] ||= {}
108
+ self['tags']['cloud_maker_config'] = self.config_name
109
+ end
110
+
111
+ def config_name
112
+ files = self.imports.dup
113
+ files.push self.extra_options['config_path'] if self.extra_options['config_path']
114
+
115
+ files.reverse.map { |import|
116
+ import[import.rindex('/')+1..-1].gsub(/\..*/, '').gsub(/[^\w]/, '-')
117
+ }.join(':')
90
118
  end
91
119
 
92
120
  # Public: Check if the CloudMaker config is in a valid state.
@@ -208,14 +236,42 @@ module CloudMaker
208
236
  # options - Any options to pass through as options to CloudMaker::Config::initialize
209
237
  #
210
238
  # Returns a new Config
211
- # Raises: Exception if the file doesn't exist.
239
+ # Raises: GitHubContentNotFound, HTTPContentNotFound, or FileContentNotFound if the file doesn't exist.
212
240
  # Raises: SyntaxError if the YAML file is invalid.
213
241
  def from_yaml(instance_config_yaml, options={})
214
- begin
215
- full_path = File.expand_path(instance_config_yaml)
216
- cloud_yaml = File.open(full_path, "r") #Right_AWS will base64 encode this for us
217
- rescue
218
- raise "ERROR: The path to the CloudMaker config is incorrect"
242
+ if instance_config_yaml =~ /\Agithub:\/\//
243
+ begin
244
+ user, repo, *path = instance_config_yaml.split('/')[2..-1]
245
+ begin
246
+ if options['github_token']
247
+ response = RestClient.get(
248
+ "https://api.github.com/repos/#{user}/#{repo}/contents/#{path.join('/')}",
249
+ "Authorization" => "token #{options['github_token']}"
250
+ )
251
+ else
252
+ response = RestClient.get("https://api.github.com/repos/#{user}/#{repo}/contents/#{path.join('/')}")
253
+ end
254
+ cloud_yaml = Base64.decode64(JSON.parse(response)['content'])
255
+ rescue
256
+ raise GitHubContentNotFound.new(
257
+ "Unable to access the configuration #{instance_config_yaml} from GitHub.",
258
+ instance_config_yaml
259
+ )
260
+ end
261
+ end
262
+ elsif instance_config_yaml =~ /\Ahttps?:\/\//
263
+ begin
264
+ cloud_yaml = RestClient.get(instance_config_yaml)
265
+ rescue
266
+ raise HTTPContentNotFound.new("Unable to access the configuration via HTTP from #{instance_config_yaml}.", instance_config_yaml)
267
+ end
268
+ else
269
+ begin
270
+ full_path = File.expand_path(instance_config_yaml)
271
+ cloud_yaml = File.open(full_path, "r") #Right_AWS will base64 encode this for us
272
+ rescue
273
+ raise FileContentNotFound.new("Unable to access the configuration via your local file system from #{full_path}.", instance_config_yaml)
274
+ end
219
275
  end
220
276
 
221
277
  # loading a blank config file returns false, it's an odd degenerate case but handling
@@ -340,6 +396,7 @@ module CloudMaker
340
396
  def import(cloud_maker_config)
341
397
  self.options = cloud_maker_config.options.deep_merge!(self.options)
342
398
  self.includes = cloud_maker_config.includes.concat(self.includes).uniq
399
+ self.imports = cloud_maker_config.imports.concat(self.imports).uniq
343
400
  self.cloud_config = cloud_maker_config.cloud_config.deep_merge!(self.cloud_config)
344
401
  self.extra_options = cloud_maker_config.extra_options.deep_merge!(self.extra_options)
345
402
  end
@@ -100,7 +100,6 @@ module CloudMaker
100
100
  else
101
101
  region = ec2 # .instances.create will just put things in the default region
102
102
  end
103
-
104
103
  instance = region.instances.create(
105
104
  :image_id => cloud_maker_config['ami'],
106
105
  :security_groups => cloud_maker_config['security_group'],
@@ -110,7 +109,7 @@ module CloudMaker
110
109
  :user_data => user_data
111
110
  )
112
111
 
113
- instance.tags.set(cloud_maker_config["tags"]) if cloud_maker_config["tags"]
112
+ instance.tags.set(cloud_maker_config['tags']) if cloud_maker_config['tags']
114
113
  instance.associate_elastic_ip(cloud_maker_config["elastic_ip"]) if cloud_maker_config["elastic_ip"]
115
114
 
116
115
  archiver = S3Archiver.new(
@@ -174,7 +173,8 @@ module CloudMaker
174
173
  :private_ip_address => instance.private_ip_address,
175
174
  :key_name => instance.key_name,
176
175
  :owner_id => instance.owner_id,
177
- :status => instance.status
176
+ :status => instance.status,
177
+ :tags => instance.tags.inject({}) {|hash, tag| hash[tag.first] = tag.last;hash}
178
178
  }
179
179
  end
180
180
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cloud-maker
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2012-08-15 00:00:00.000000000 Z
13
+ date: 2012-08-17 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: colorize
@@ -128,5 +128,5 @@ rubyforge_project:
128
128
  rubygems_version: 1.8.24
129
129
  signing_key:
130
130
  specification_version: 3
131
- summary: Launch and perform initial configuration of cloud servers.
131
+ summary: Extends Ubuntu CloudInit to launch and configure cloud servers.
132
132
  test_files: []