hazetug 0.1.1

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.
@@ -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__