Class: CloudMaker::Config

Inherits:
Object
  • Object
show all
Defined in:
lib/cloud_maker/config.rb

Defined Under Namespace

Classes: ContentNotFound, FileContentNotFound, GitHubContentNotFound, HTTPContentNotFound

Constant Summary collapse

CLOUD_CONFIG_HEADER =

Internal: A mime header for the Cloud Init config section of the user data

%Q|Content-Type: text/cloud-config; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="cloud-config.yaml"\n\n|
INCLUDES_HEADER =

Internal: A mime header for the includes section of the user data

%Q|Content-Type: text/x-include-url; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="includes.txt"\n\n|
BOOTHOOK_HEADER =

Internal: A mime header for the Cloud Boothook section of the user data

%Q|Content-Type: text/cloud-boothook; charset="us-ascii"\nMIME-Version: 1.0\nContent-Transfer-Encoding: 7bit\nContent-Disposition: attachment; filename="boothook.sh"\n\n|
MULTIPART_HEADER =

Internal: A multipart mime header for describing the entire user data content. It includes a placeholder for the boundary text ‘_boundary___’ that needs to be replaced in the actual mime document.

%Q|Content-Type: multipart/mixed; boundary="___boundary___"\nMIME-Version: 1.0\n\n|
DEFAULT_KEY_PROPERTIES =

Internal: If you don’t specify a property associated with a key in the cloud_maker config file we will use these properties to fill in the blanks

{
  "environment" => true,
  "required" => false,
  "value" => nil,
  "default" => nil,
  "description" => nil
}

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(cloud_config, extra_options = {}) ⇒ Config

Public: Initializes a new CloudMaker object.

cloud_config - A Hash describing all properties of the CloudMaker config.

'cloud_maker' - The configuration properties for CloudMaker. These can
                be specified either as
                   key: value
                or as
                   key: {
                     environment: boolean,
                     required: boolean,
                     value: value
                   }
                If specified as key: value DEFAULT_KEY_PROPERTIES will
                be used. If the detailed version is used all properties
                are optional and DEFAULT_KEY_PROPERTIES will be used to
                fill in the blanks.
'include'     - An array of URLs or a String containing 1 URL per line
                with optional # prefixed lines as comments.
...           - All valid properties of a Cloud Init config
                are also valid here. See:
                https://help.ubuntu.com/community/CloudInit

extra_options - Options that describe the config as opposed to being part

                of the config.
'config_path' - The path the config was loaded from. Used for archival purposes.
'import_ec2'  - CloudMaker::EC2 defines properties it relies on, if this value
                is true then we pull those property definitions in.

Returns a CloudMaker object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# File 'lib/cloud_maker/config.rb', line 91

def initialize(cloud_config, extra_options={})
  self.extra_options = extra_options
  cloud_config = cloud_config.dup

  self.options = extract_cloudmaker_config!(cloud_config)
  self.includes = extract_includes!(cloud_config)
  self.imports = extract_imports!(cloud_config)
  self.cloud_config = cloud_config

  self.import(self.class.new(EC2::CLOUD_MAKER_CONFIG, 'config_path' => "EC2")) if (extra_options['import_ec2'])

  # It's important here that reverse duplicates the imports array as executing the import will
  # add the imported configs imports to the list and we do NOT want to reimport those as well.
  self.imports.reverse.each do |import_path|
    self.import(self.class.from_yaml(import_path, self.extra_options.merge('import_ec2' => false)))
  end
  self['tags'] ||= {}
  self['tags']['cloud_maker_config'] = self.config_name
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method = nil, *args) ⇒ Object



111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/cloud_maker/config.rb', line 111

def method_missing(method=nil, *args)
  if method.to_s[-1] == "?"
    key_name = method.to_s[0, method.to_s.length-1]
    value = self[key_name]
    if value.respond_to? :empty?
      !value.empty?
    else
      !value.nil?
    end
  else
    super
  end
end

Instance Attribute Details

#cloud_configObject

Public: Gets/Sets the Hash of Cloud Init properties. See help.ubuntu.com/community/CloudInit for valid options



31
32
33
# File 'lib/cloud_maker/config.rb', line 31

def cloud_config
  @cloud_config
end

#extra_optionsObject

Public: Gets/Sets extra information about the config to be stored for archival purposes.



37
38
39
# File 'lib/cloud_maker/config.rb', line 37

def extra_options
  @extra_options
end

#importsObject

Internal: Gets/Sets an array of paths to other cloud maker configs to import.



39
40
41
# File 'lib/cloud_maker/config.rb', line 39

def imports
  @imports
end

#includesObject

Public: Gets/Sets an Array of URLs to be included, this corresponds to the list of URLs in a Cloud Init includes file.



34
35
36
# File 'lib/cloud_maker/config.rb', line 34

def includes
  @includes
end

#optionsObject

Public: Gets/Sets the CloudMaker specific properties. The options hash is formatted as:

{
  'key1' => {
     'environment': boolean,
     'required': boolean,
     'value': value
  },
  'key2' => { ... },
  ...
}


28
29
30
# File 'lib/cloud_maker/config.rb', line 28

def options
  @options
end

Class Method Details

.from_yaml(instance_config_yaml, options = {}) ⇒ Object

Public: Takes the path of a YAML file and loads a new Config object from it.

instance_config_yaml - The path of the YAML file options - Any options to pass through as options to CloudMaker::Config::initialize

Returns a new Config Raises: GitHubContentNotFound, HTTPContentNotFound, or FileContentNotFound if the file doesn’t exist. Raises: SyntaxError if the YAML file is invalid.



259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
# File 'lib/cloud_maker/config.rb', line 259

def from_yaml(instance_config_yaml, options={})
  if instance_config_yaml =~ /\Agithub:\/\//
    begin
      user, repo, *path = instance_config_yaml.split('/')[2..-1]
      begin
        if options['github_token']
          response = RestClient.get(
            "https://api.github.com/repos/#{user}/#{repo}/contents/#{path.join('/')}",
            "Authorization" => "token #{options['github_token']}"
          )
        else
          response = RestClient.get("https://api.github.com/repos/#{user}/#{repo}/contents/#{path.join('/')}")
        end
        cloud_yaml = Base64.decode64(JSON.parse(response)['content'])
      rescue
        raise GitHubContentNotFound.new(
          "Unable to access the configuration #{instance_config_yaml} from GitHub.",
          instance_config_yaml
        )
      end
    end
  elsif instance_config_yaml =~ /\Ahttps?:\/\//
    begin
      cloud_yaml = RestClient.get(instance_config_yaml)
    rescue
      raise HTTPContentNotFound.new("Unable to access the configuration via HTTP from #{instance_config_yaml}.", instance_config_yaml)
    end
  else
    begin
      instance_config_yaml = File.expand_path(instance_config_yaml)
      cloud_yaml = File.open(instance_config_yaml, "r") #Right_AWS will base64 encode this for us
    rescue
      raise FileContentNotFound.new("Unable to access the configuration via your local file system from #{instance_config_yaml}.", instance_config_yaml)
    end
  end

  # loading a blank config file returns false, it's an odd degenerate case but handling
  # it like this makes sanity checking other missing values easy.
  config = YAML::load(cloud_yaml) || {}

  CloudMaker::Config.new(config, options.merge('config_path' => instance_config_yaml))
end

Instance Method Details

#[](key) ⇒ Object

Public: Access values in the cloudmaker options object

key - The key of the property you’re accessing

Returns the value property for options



224
225
226
# File 'lib/cloud_maker/config.rb', line 224

def [](key)
  self.options[key] ? self.options[key]["value"] : nil
end

#[]=(key, val) ⇒ Object

Public: Sets the value property for key in the cloudmaker options hash

key - The key of the property you’re accessing val - The value you wish to assign to the key

Returns val



234
235
236
237
238
239
240
241
# File 'lib/cloud_maker/config.rb', line 234

def []=(key, val)
  if (self.options[key])
    self.options[key]["value"] = val
  else
    self.options[key] = DEFAULT_KEY_PROPERTIES.merge('value' => val)
  end
  val
end

#advanced_config?(value) ⇒ Boolean

Internal: Determines if value should be treated as a value or a property configuration hash. A property configuration hash would specify at least one of environment, value, or required.

value - The value to evaluate

Returns true if value is a property configuration, and false if it’s just

a value

Returns:

  • (Boolean)


341
342
343
# File 'lib/cloud_maker/config.rb', line 341

def advanced_config?(value)
  value.kind_of?(Hash) && !(DEFAULT_KEY_PROPERTIES.keys & value.keys).empty?
end

#boothook_dataObject

Public: Generates a shell script to set environment variables for all CloudMaker config options, will get executed at machine boot.

Returns a String containing the shell script



197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/cloud_maker/config.rb', line 197

def boothook_data
  env_run_cmds = []

  self.options.each_pair do |key, properties|
    if properties["environment"] && !properties["value"].nil?
      env_run_cmds.push(set_environment_variable_cmd(key, properties["value"]))
    end
  end
  env_run_cmds.push("touch /var/lib/cloud/cloud_init_complete")

  return "#cloud-boothook\n#!/bin/sh\nif [ ! -f /var/lib/cloud/cloud_init_complete ]; then\n#{env_run_cmds.join("\n")}\nfi\n"
end

#cloud_config_dataObject

Public: Generates a cloud-init configuration

Returns a String containing the cloud init configuration in YAML format



189
190
191
# File 'lib/cloud_maker/config.rb', line 189

def cloud_config_data
  return "#cloud-config\n#{self.cloud_config.to_yaml}"
end

#config_nameObject



125
126
127
128
129
130
131
132
133
134
135
# File 'lib/cloud_maker/config.rb', line 125

def config_name
  files = self.imports.dup
  files.push self.extra_options['config_path'] if self.extra_options['config_path']

  files.reverse.map { |import|
    if import.rindex('/')
      import = import[import.rindex('/')+1..-1]
    end
    import.gsub(/\..*/, '').gsub(/[^\w]/, '-')
  }.join(':')
end

#extract_cloudmaker_config!(config) ⇒ Object

Internal: Takes a CloudMaker config and parses out the CloudMaker specific portions of it. For each key/value it fills in any property blanks from the DEFAULT_KEY_PROPERTIES. It also deletes the cloud_maker property from config.

config - A hash that should contain a ‘cloud_maker’ key storing CloudMaker

configuration properties.

Returns a Hash in the format of

{'key1' => {
   'environment' => ...,
   'value' => ...,
   'required' ...
}, 'key2' => ... , ...}


318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/cloud_maker/config.rb', line 318

def extract_cloudmaker_config!(config)
  cloud_maker_config = config.delete('cloud-maker') || {}
  cloud_maker_config.keys.each do |key|
    #if key is set to anything but a hash then we treat it as the value property
    if !advanced_config?(cloud_maker_config[key])
      cloud_maker_config[key] = {
        "value" => cloud_maker_config[key]
      }
    end

    cloud_maker_config[key] = DEFAULT_KEY_PROPERTIES.merge(cloud_maker_config[key])
  end
  cloud_maker_config
end

#extract_imports!(config) ⇒ Object

Internal: Takes a CloudMaker config and parses out the imports list.

config - A hash that should contain an ‘import’ key storing an array of paths to import.

Returns an Array of URLs



374
375
376
# File 'lib/cloud_maker/config.rb', line 374

def extract_imports!(config)
  config.delete('import') || []
end

#extract_includes!(config) ⇒ Object

Internal: Takes a CloudMaker config and parses out the includes list. If the list is an array it treats each entry as a URL. If it is a string it treats the string as the contents of a Cloud Init include file.

config - A hash that should contain an ‘include’ key storing the include information.

Returns an Array of URLs



353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/cloud_maker/config.rb', line 353

def extract_includes!(config)
  includes = config.delete('include')

  #if we didn't specify it just use a blank array
  if includes.nil?
    includes = []
  #if we passed something other than an array turn it into a string and split it up into urls
  elsif !includes.kind_of?(Array)
    includes = includes.to_s.split("\n")
    includes.reject! {|line| line.strip[0] == "#" || line.strip.empty?}
  end

  includes
end

#import(cloud_maker_config) ⇒ Object

Internal: Deep merges another CloudMaker::Config into this one giving precedence to all values set in this one. Arrays will be merged as imported_array.concat(current_array).uniq.

“–” can be used as a knockout prefix, ie. “–foo” will delete the key “foo” when an array is merged, or “–” will delete the entire existing contents of the array.

It should be noted that this is not reference safe, ie. objects within cloud_maker_config will end up referenced from this config object as well.

cloud_maker_config - The CloudMaker::Config to be imported

Returns nothing.



426
427
428
429
430
431
432
433
434
435
436
437
438
439
# File 'lib/cloud_maker/config.rb', line 426

def import(cloud_maker_config)
  [:options, :includes, :imports, :cloud_config, :extra_options].each do |key|
  key = key.to_sym
  #toss both of these into a hash because deep_merge only works on hashes
  cloud_maker_config_hash = {:value => cloud_maker_config.send(key)}
  self_hash = {:value => self.send(key)}
  self.send(
    :"#{key}=",
    cloud_maker_config_hash.deep_merge!(
      self_hash, :preserve_unmergeables => false, :knockout_prefix => '--'
    )[:value]
  )
  end
end

#includes_dataObject

Public: Generates a cloud-init includes list

Returns a String containing the cloud init includes list



214
215
216
# File 'lib/cloud_maker/config.rb', line 214

def includes_data
  ["#include", *self.includes.map(&:to_s)].join("\n")
end

#inspectObject

Returns a String representation of the CloudMaker config



244
245
246
# File 'lib/cloud_maker/config.rb', line 244

def inspect
  "CloudMakerConfig#{self.options.inspect}"
end

#missing_valuesObject

Public: Finds a list of keys in the CloudMaker config that are required to have a value but do not yet have one.

Returns an Array of required keys that are missing values.



149
150
151
# File 'lib/cloud_maker/config.rb', line 149

def missing_values
  self.options.select {|key, option| (option["required"] || option["default"]) && option["value"].nil?}.map(&:first).map(&:dup)
end

#set_environment_variable_cmd(key, value) ⇒ Object

Internal: Generates the shell command necessary to set an environment variable. It escapes the value but assumes there are no special characters in the key. If value is an array or a hash it generates an environment variable for each value in the array/hash with the key set to key_index.

key - The key of the environment variable value - The value to set the key to

Returns a string that can be executed to globally set the environment variable.



387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
# File 'lib/cloud_maker/config.rb', line 387

def set_environment_variable_cmd(key, value)
  if (value.kind_of?(Hash))
    strings = []
    value.keys.each { |hash_key|
      strings.push(set_environment_variable_cmd("#{key}_#{hash_key}", value[hash_key]))
    }
    strings.push(set_environment_variable_cmd("#{key}_length", value.keys.join(' ')))
    strings.push(set_environment_variable_cmd("#{key}_json", value.to_json))
    strings.join(';')

  elsif (value.kind_of?(Array))
    strings = []
    value.each_with_index { |arr_val, i|
      strings.push(set_environment_variable_cmd("#{key}_#{i}", arr_val))
    }
    strings.push(set_environment_variable_cmd("#{key}_length", value.length))
    strings.push(set_environment_variable_cmd("#{key}_json", value.to_json))
    strings.join(';')
  else
    underscored_key = key.to_s.gsub(/[^a-zA-Z0-9_]/, '_')
    escaped_value = value.to_s.gsub(/"/, '\\\\\\\\\"')
    "echo \"#{underscored_key}=\\\"#{escaped_value}\\\"\" >> /etc/environment"
  end
end

#to_hashObject

Returns a Hash of all of the CloudMaker specific properties in the configuration.



154
155
156
157
158
159
160
161
162
# File 'lib/cloud_maker/config.rb', line 154

def to_hash
  {
    'cloud-maker' => self.options.map {|key, properties| [key, properties["value"]]},
    'cloud-init' => self.cloud_config,
    'include' => self.includes,
    'import' => self.imports,
    'extra-options' => self.extra_options
  }
end

#to_user_dataObject

Public: Generates a multipart userdata string suitable for use with Cloud Init on EC2

Returns a String containing the mime encoded userdata



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
# File 'lib/cloud_maker/config.rb', line 167

def to_user_data
  # build a multipart document
  parts = []

  parts.push(BOOTHOOK_HEADER + boothook_data)
  parts.push(CLOUD_CONFIG_HEADER + cloud_config_data)
  parts.push(INCLUDES_HEADER + includes_data)

  #not that it's likely but lets make sure that we don't choose a boundary that exists in the document.
  boundary = ''
  while parts.any? {|part| part.index(boundary)}
    boundary = "===============#{rand(8999999999999999999) + 1000000000000000000}=="
  end

  header = MULTIPART_HEADER.sub(/___boundary___/, boundary)

  return [header, *parts].join("\n--#{boundary}\n") + "\n--#{boundary}--"
end

#valid?Boolean

Public: Check if the CloudMaker config is in a valid state.

Returns true if and only if all required properties have non-nil values

and false otherwise.

Returns:

  • (Boolean)


141
142
143
# File 'lib/cloud_maker/config.rb', line 141

def valid?
  self.options.all? {|key, option| !(option["required"] && option["value"].nil?)}
end