hazetug 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,71 @@
1
+ require 'hazetug/haze/cloud_server'
2
+
3
+ class Hazetug
4
+ class Haze
5
+ class DigitalOcean < Haze
6
+ include CloudServer
7
+
8
+ requires :name, :location, :flavor, :image
9
+ defaults :backups_active => true, :private_networking => true
10
+
11
+ def initialize(config={})
12
+ super
13
+ @config[:bits] = bits_from_string(@config[:image])
14
+ @config[:image_slug] = image_from_string(@config[:image])
15
+ end
16
+
17
+ def create_server_args
18
+ {
19
+ :name => config[:name],
20
+ :region_id => lookup(:location).id,
21
+ :flavor_id => lookup(:flavor).id,
22
+ :image_id => lookup(:image).id,
23
+ :ssh_key_ids => ssh_key_ids,
24
+ :backups_active => config[:backups_active],
25
+ :private_networking => config[:private_networking]
26
+ }
27
+ end
28
+
29
+ def compare_location?(o)
30
+ a = o.attributes
31
+ value = config[:location]
32
+ a[:name].match(/^#{value}/i) != nil ||
33
+ a['slug'] == config[:location] || a['slug'].match(/^#{value}/) != nil
34
+ end
35
+
36
+ def compare_flavor?(o)
37
+ a = o.attributes
38
+ value = config[:flavor]
39
+ a[:memory] == memory_in_megabytes(value) ||
40
+ a[:name].match(/^#{value}/i)
41
+ end
42
+
43
+ def compare_image?(o)
44
+ a = o.attributes
45
+ img_id = image_from_string(a[:name])
46
+ bits = bits_from_string(a[:name])
47
+ img_id.match(/^#{config[:image_slug]}/) && bits == config[:bits]
48
+ end
49
+
50
+ def public_ip_address
51
+ server and server.public_ip_address
52
+ end
53
+
54
+ def private_ip_address
55
+ server and server.private_ip_address
56
+ end
57
+
58
+ private
59
+
60
+ def ssh_key_ids
61
+ if config[:ssh_keys].nil? || config[:ssh_keys].empty?
62
+ compute.ssh_keys.map(&:id)
63
+ else
64
+ config[:ssh_keys].map do |n|
65
+ k = lookup(:ssh_key, n) and k.id
66
+ end.compact
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,66 @@
1
+ require 'securerandom'
2
+ require 'hazetug/haze/cloud_server'
3
+
4
+ class Hazetug
5
+ class Haze
6
+ class Linode < Haze
7
+ include CloudServer
8
+
9
+ requires :name, :location, :flavor, :image
10
+ defaults :payment_terms => 1
11
+
12
+ def initialize(config={})
13
+ super
14
+ @config[:bits] = bits_from_string(@config[:image])
15
+ @config[:image_slug] = image_from_string(@config[:image])
16
+ @config[:ssh_password] ||= SecureRandom.hex
17
+ end
18
+
19
+ def create_server_args
20
+ latest = /^Latest #{config[:bits]} bit/
21
+ {
22
+ :name => config[:name],
23
+ :data_center => lookup(:location),
24
+ :flavor => lookup(:flavor),
25
+ :image => lookup(:image),
26
+ :kernel => compute.kernels.find {|k| k.name.match(latest)},
27
+ :password => config[:ssh_password],
28
+ :payment_terms => config[:payment_terms]
29
+ }
30
+ end
31
+
32
+ def compare_location?(o)
33
+ a = o.attributes
34
+ value = config[:location]
35
+ a[:location].match(/^#{value}/i) != nil ||
36
+ a[:abbr] == config[:location] || a[:abbr].match(/^#{value}/) != nil
37
+ end
38
+
39
+ def compare_flavor?(o)
40
+ a = o.attributes
41
+ value = config[:flavor]
42
+ a[:ram] == memory_in_megabytes(value) ||
43
+ a[:name].match(/^#{value}/i)
44
+ end
45
+
46
+ def compare_image?(o)
47
+ a = o.attributes
48
+ img_id = image_from_string(a[:name])
49
+ bits = a[:bits]
50
+ img_id.match(/^#{config[:image_slug]}/) && bits == config[:bits]
51
+ end
52
+
53
+ def public_ip_address
54
+ server and server.public_ip_address
55
+ end
56
+
57
+ def private_ip_address
58
+ server and begin
59
+ found = server.ips.find {|ip| not ip.public}
60
+ found and found.ip
61
+ end
62
+ end
63
+
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,81 @@
1
+ require 'hazetug/config'
2
+ require 'hazetug/compute'
3
+ require 'hazetug/ui'
4
+ require 'hazetug/tug'
5
+ require 'chef/mash'
6
+
7
+ class Hazetug
8
+ class Haze
9
+ include Hazetug::UI::Mixin
10
+
11
+ RE_BITS = /-?x(32)$|-?x(64)$|(32)bit|(64)bit/i
12
+ attr_reader :config, :compute, :compute_name, :server
13
+
14
+ def initialize(config={})
15
+ @compute_name = Hazetug.leaf_klass_name(self.class)
16
+ @compute = Hazetug::Compute.const_get(compute_name).new
17
+ @config = configure(config)
18
+ @server = nil
19
+ @sshable = false
20
+ end
21
+
22
+ def provision
23
+ provision_server
24
+ wait_for_ssh
25
+ rescue Fog::Errors::Error
26
+ ui.error "[#{compute_name}] #{$!.inspect}"
27
+ ui.msg $@
28
+ exit(1)
29
+ end
30
+
31
+ def configure(config)
32
+ input = config.keys.map(&:to_sym)
33
+ requires = self.class.requires
34
+ unless (norequired = requires.select {|r| not input.include?(r)}).empty?
35
+ ui.error "Required options missing: #{norequired.join(', ')}"
36
+ raise ArgumentError, "Haze options missing"
37
+ end
38
+ Mash.new(self.class.defaults.merge(config))
39
+ end
40
+
41
+ class << self
42
+ def requires(*args)
43
+ if args.empty?
44
+ @requires
45
+ else
46
+ @requires = args.flatten.dup
47
+ end
48
+ end
49
+
50
+ def defaults(hash=nil)
51
+ if hash.nil?
52
+ @defaults
53
+ else
54
+ @defaults = hash
55
+ end
56
+ end
57
+
58
+ def [](haze_name)
59
+ klass = Hazetug.camel_case_name(haze_name)
60
+ Hazetug::Haze.const_get(klass)
61
+ end
62
+ end
63
+
64
+ def public_ip_address
65
+ end
66
+
67
+ def private_ip_address
68
+ end
69
+
70
+ protected
71
+
72
+ def provision_server
73
+ ui.error "#{compute_name} Provisioning is not impemented"
74
+ end
75
+
76
+ def wait_for_ssh
77
+ ui.error "#{compute_name} Waiting for shh is not impemented"
78
+ end
79
+
80
+ end
81
+ end
@@ -0,0 +1,31 @@
1
+ require 'hazetug/version'
2
+ require 'hazetug/compute'
3
+ require 'hazetug/haze'
4
+ require 'hazetug/haze/linode'
5
+ require 'hazetug/haze/digital_ocean'
6
+
7
+ class Hazetug
8
+ class Exception < ::Exception
9
+ end
10
+
11
+ class << self
12
+ def camel_case_name(string_or_symbol)
13
+ string_or_symbol.to_s.split('_').map(&:capitalize).join
14
+ end
15
+
16
+ def leaf_klass_name(klass)
17
+ if klass.is_a? String
18
+ klass.split('::').last
19
+ else
20
+ klass.name.split('::').last
21
+ end
22
+ end
23
+
24
+ def ssh_keys(compute_name=nil)
25
+ ssh_attr = []
26
+ ssh_attr << "#{compute_name.downcase}_ssh_keys" if compute_name
27
+ ssh_attr << "ssh_keys"
28
+ Hazetug::Config[ssh_attr.find {|a| Hazetug::Config[a]}]
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,53 @@
1
+ require 'psych'
2
+ require 'chef/mash'
3
+ require 'chef/mixin/deep_merge'
4
+ require 'hazetug/ui'
5
+
6
+ class Hazetug
7
+ class Task
8
+ include Hazetug::UI::Mixin
9
+
10
+ def initialize(path)
11
+ path = File.expand_path(path)
12
+ @task = Mash.new(Psych.load_file(path))
13
+ rescue Psych::Exception
14
+ ui.fatal "Unable to parse hazetug task file: '#{path}'"
15
+ puts $!.inspect, $@
16
+ exit(1)
17
+ rescue SystemCallError
18
+ ui.fatal "Unable to read file: '#{path}'"
19
+ exit(1)
20
+ end
21
+
22
+ def [](key)
23
+ @task[key]
24
+ end
25
+
26
+ def hosts_to_bootstrap(env={}, &block)
27
+ return if block.nil?
28
+ base_conf = Mash.new(task)
29
+ hosts = base_conf.delete(:bootstrap)
30
+ base_conf = Chef::Mixin::DeepMerge.merge(base_conf, env)
31
+ hosts.each do |conf|
32
+ merged = Chef::Mixin::DeepMerge.merge(base_conf, conf)
33
+ block.call(merged)
34
+ end
35
+ end
36
+
37
+ class << self
38
+ protected :new
39
+
40
+ def load_from_file(path)
41
+ @instance ||= self.new(path)
42
+ end
43
+
44
+ def [](key)
45
+ @instance and @instance[key]
46
+ end
47
+ end
48
+
49
+ protected
50
+ attr_reader :task
51
+
52
+ end
53
+ end
@@ -0,0 +1,97 @@
1
+ require 'chef/knife/ssh'
2
+ require 'chef/knife/bootstrap'
3
+ require 'hazetug/tug'
4
+
5
+ # Monkey Patch:)
6
+ # Extend knife bootstrap context with our data
7
+ class Chef::Knife::Core::BootstrapContext
8
+ def hazetug; @config[:hazetug]; end
9
+ end
10
+
11
+ class Chef::Knife::Ssh < Chef::Knife
12
+ def run
13
+ extend Chef::Mixin::Command
14
+ @longest = 0
15
+ configure_attribute
16
+ configure_user
17
+ configure_password
18
+ configure_identity_file
19
+ configure_gateway
20
+ configure_session
21
+ exit_status = ssh_command(@name_args[1..-1].join(" "))
22
+ session.close
23
+
24
+ exit_status
25
+ end
26
+ end
27
+
28
+ class Hazetug
29
+ class Tug
30
+ class Knife < Tug
31
+
32
+ def bootstrap_server
33
+ [
34
+ :template_file,
35
+ :identity_file,
36
+ :ssh_user,
37
+ :ssh_password,
38
+ :host_key_verify
39
+ ].each do |opt|
40
+ kb.config[opt] = bootstrap_options[opt]
41
+ end
42
+ [
43
+ :environment,
44
+ :chef_server_url,
45
+ :validation_key
46
+ ].each do |opt|
47
+ Chef::Config[opt] = bootstrap_options[opt]
48
+ end
49
+ kb.name_args = [haze.server.ssh_ip_address]
50
+ kb.run
51
+ ensure
52
+ @kb and @kb.ui.stdout.close
53
+ end
54
+
55
+ def kb
56
+ @kb ||= begin
57
+ lf = create_log_file
58
+ Chef::Knife::Bootstrap.load_deps
59
+ kb = Chef::Knife::Bootstrap.new
60
+ kb.config[:hazetug] = config
61
+ kb.ui = Chef::Knife::UI.new(lf, lf, lf, {verbosity: 2})
62
+ kb
63
+ end
64
+ end
65
+
66
+ def bootstrap_options
67
+ @bootstrap_options ||= begin
68
+ template = options[:opts][:bootstrap]
69
+ opts = {}
70
+ opts[:validation_key] = File.expand_path(config[:chef_validation_key] || 'validation.pem')
71
+ opts[:template_file] = File.expand_path(template || 'bootstrap.erb')
72
+ opts[:ssh_user] = config[:ssh_user] || 'root'
73
+ opts[:ssh_password] = config[:ssh_password]
74
+ opts[:environment] = config[:chef_environment]
75
+ opts[:host_key_verify] = config[:host_key_verify] || false
76
+ opts[:chef_server_url] = config[:chef_server_url]
77
+ opts[:identity_file] = preferred_ssh_identity if not opts[:ssh_password]
78
+ opts
79
+ end
80
+ end
81
+
82
+ def preferred_ssh_identity
83
+ @preferred_ssh_identity ||= begin
84
+ compute = Hazetug.leaf_klass_name(haze.class.name).downcase
85
+ identity = config[:identity_file]
86
+ key_path = (Hazetug::Config["#{compute}_ssh_keys"] || []).first
87
+ if identity.nil? && key_path.nil?
88
+ raise Hazetug::Exception, "identity file not specified, use #{compute}_ssh_keys or identity_file"
89
+ end
90
+ identity ||= File.expand_path(key_path)
91
+ identity
92
+ end
93
+ end
94
+
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,85 @@
1
+ require 'hazetug/tug/knife'
2
+ require 'hazetug/ui'
3
+ require 'chef/mash'
4
+
5
+ class Hazetug
6
+ class Tug
7
+ include Hazetug::UI::Mixin
8
+
9
+ SSH_OPTIONS = [
10
+ :ssh_user, :ssh_password, :ssh_port,
11
+ :ssh_host_key_verify, :ssh_keys
12
+ ]
13
+ LOGDIR = "#{Dir.pwd}/logs"
14
+
15
+ attr_reader :haze, :config, :options
16
+
17
+ def initialize(config={}, haze=nil)
18
+ @haze = haze
19
+ @config = config
20
+ end
21
+
22
+ def tug_name
23
+ @tug_name ||= self.class.name.split('::').last
24
+ end
25
+
26
+ def bootstrap(options={})
27
+ if haze && haze.server && haze.server.sshable?
28
+ @options = options
29
+ haztug_set_variables
30
+ ip = config[:public_ip_address]
31
+ ui.msg "[#{tug_name}] bootstraping server #{haze.config[:name]}, ip: #{ip}"
32
+ exit_status = bootstrap_server
33
+ if exit_status.is_a?(Fixnum) && exit_status != 0
34
+ ui.error "[#{tug_name}] bootstraping server #{haze.config[:name]} failed."
35
+ else
36
+ ui.msg "[#{tug_name}] bootstraping server #{haze.config[:name]} done."
37
+ end
38
+ else
39
+ ui.error "#{haze.compute_name} skipping bootstrap, server #{haze.config[:name]} not ready"
40
+ end
41
+ rescue Hazetug::Exception => e
42
+ ui.error "[#{haze.compute_name}] #{e.message}"
43
+ end
44
+
45
+ class << self
46
+ def [](symbol_or_string)
47
+ klass = Hazetug.camel_case_name(symbol_or_string)
48
+ const_get(klass)
49
+ end
50
+
51
+ def ssh_options_from(config)
52
+ SSH_OPTIONS.inject({}) do |hash, k|
53
+ if value = (config[k])
54
+ hash[k] = value
55
+ end
56
+ hash
57
+ end
58
+ end
59
+ end
60
+
61
+ private
62
+
63
+ def haztug_set_variables
64
+ {
65
+ compute_name: haze.compute_name.downcase,
66
+ public_ip_address: (haze.public_ip_address || haze.server.ssh_ip_address rescue nil),
67
+ private_ip_address: haze.private_ip_address
68
+ }.each do |key, value|
69
+ config[key] = value if value
70
+ end
71
+ end
72
+
73
+ protected
74
+
75
+ def create_log_file
76
+ unless File.directory?(LOGDIR)
77
+ Dir.mkdir(LOGDIR)
78
+ end
79
+ log = File.new("#{LOGDIR}/#{haze.config[:name]}", "w+")
80
+ log.sync = true
81
+ log
82
+ end
83
+
84
+ end
85
+ end
data/lib/hazetug/ui.rb ADDED
@@ -0,0 +1,75 @@
1
+ class Hazetug
2
+ class UI
3
+ module Mixin
4
+ def self.included(includer)
5
+ includer.class_exec do
6
+ define_method(:ui) {Hazetug::UI.instance}
7
+ end
8
+ end
9
+ end
10
+
11
+ attr_reader :stdout
12
+ attr_reader :stderr
13
+ attr_reader :stdin
14
+
15
+ def initialize(stdout, stderr, stdin)
16
+ @stdout, @stderr, @stdin = stdout, stderr, stdin
17
+ end
18
+
19
+ def highline
20
+ @highline ||= begin
21
+ require 'highline'
22
+ HighLine.new
23
+ end
24
+ end
25
+
26
+ def msg(message)
27
+ begin
28
+ stdout.puts message
29
+ rescue Errno::EPIPE => e
30
+ raise e
31
+ exit 0
32
+ end
33
+ end
34
+
35
+ alias :info :msg
36
+
37
+ def err(message)
38
+ begin
39
+ stderr.puts message
40
+ rescue Errno::EPIPE => e
41
+ raise e
42
+ exit 0
43
+ end
44
+ end
45
+
46
+ def warn(message)
47
+ err("#{color('WARNING:', :yellow, :bold)} #{message}")
48
+ end
49
+
50
+ def error(message)
51
+ err("#{color('ERROR:', :red, :bold)} #{message}")
52
+ end
53
+
54
+ def fatal(message)
55
+ err("#{color('FATAL:', :red, :bold)} #{message}")
56
+ end
57
+
58
+ def color(string, *colors)
59
+ if color?
60
+ highline.color(string, *colors)
61
+ else
62
+ string
63
+ end
64
+ end
65
+
66
+ def color?
67
+ stdout.tty?
68
+ end
69
+
70
+ def self.instance
71
+ @instance ||= Hazetug::UI.new(STDOUT, STDERR, STDIN)
72
+ end
73
+
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ class Hazetug
2
+ VERSION = "0.1.1"
3
+ end
data/lib/hazetug.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'hazetug/hazetug'
2
+ require 'hazetug/cli'
3
+
4
+ Hazetug::CLI.new.run(ARGV) if $0 == __FILE__