hazetug 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: a5a2351211bc68264c5fe9b4780278bbbe5f419a
4
+ data.tar.gz: f1ecdb92eb86d8978754fb21da5bb0dcd38de525
5
+ SHA512:
6
+ metadata.gz: e6ca974f9bc299115e6749def4b8b847fbdb8cbc57bc5570c6fcf54450617d5c0faffcb32600021bfba8ac25d70fe3cf3d75d6ac2e41a07fbe12876e3295f16a
7
+ data.tar.gz: 4f49bcae8fc1965fe9e35c153c69b51a4ddfa2d9228e6d37adfb5fae71824afd739f346ad53d6a6f0afee4a7a343e0b0841bbc7dd3c1161af369c78a8cabe40c
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ logs
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in hazetug.gemspec
4
+ gemspec
5
+ gem 'fog', git: 'https://github.com/fog/fog.git'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Denis Barishev
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,188 @@
1
+ # Hazetug
2
+
3
+ Cloud provisioner and bootstrapper for DigitalOcean and Linode.
4
+ Hazetug uses [fog cloud library](http://fog.io) to be able to easily append other cloud computes and *tugs* (bootstraps) hosts using:
5
+
6
+ * Knife bootstrap.
7
+
8
+ ## Options
9
+
10
+ ### Cloud Computes specific options
11
+
12
+ <table>
13
+ <tr>
14
+ <td><b>Option</b></td>
15
+ <td><b>Description</b></td>
16
+ </tr>
17
+ <tr>
18
+ <td><b><i>name</i></b></td>
19
+ <td>Name of a host to provision. Random names are possible using %rand(x)% macro.</td>
20
+ </tr>
21
+ <tr>
22
+ <td><b><i>location</i></b></td>
23
+ <td>Location in the cloud compute, namely data center.</td>
24
+ </tr>
25
+ <tr>
26
+ <td><b><i>flavor</i></b></td>
27
+ <td>Flavor of a provisioned host. Flavors are recognized by memory, use: 512mb, 4gb, etc to choose the proper one.</td>
28
+ </tr>
29
+ <tr>
30
+ <td><b><i>image</i></b></td>
31
+ <td>Distribution or image which is used for provisioning. Examples: ubuntu-12.04-x32, arch-linux-2014.14. If OS architecture is not specified then x64 is assumed.</td>
32
+ </tr>
33
+ </table>
34
+
35
+ ### SSH Specific
36
+
37
+ <table>
38
+ <tr>
39
+ <td><b>Option</b></td>
40
+ <td><b>Description</b></td>
41
+ <td><b>Default value</b></td>
42
+ </tr>
43
+ <tr>
44
+ <td><b><i>ssh_user</i></b></td>
45
+ <td>User used to during provisioning and for connecting via ssh.</td>
46
+ <td><i>root</i><td>
47
+ </tr>
48
+ <tr>
49
+ <td><b><i>ssh_password</i></b></td>
50
+ <td>Password for a provisioned node. Evaluated randomly for some computes.</td>
51
+ <td></td>
52
+ </tr>
53
+ <tr>
54
+ <td><b><i>ssh_port</i></b></td>
55
+ <td>Port used for ssh connection.</td>
56
+ <td><i>22</i><td>
57
+ </tr>
58
+ <tr>
59
+ <td><b><i>host_key_verify</i></b></td>
60
+ <td>Verifies host key, set to true to enable verification.</td>
61
+ <td><i>false</i><td>
62
+ </tr>
63
+ </table>
64
+
65
+ ## Knife tug
66
+
67
+ ### Knife tug bootstrap options
68
+
69
+ <table>
70
+ <tr>
71
+ <td><b>Option</b></td>
72
+ <td><b>Description</b></td>
73
+ <td><b>Default value</b></td>
74
+ </tr>
75
+ <tr>
76
+ <td><b><i>chef_validation_key</i></b></td>
77
+ <td>Validation key used to authenticate new nodes in the Chef Server.</td>
78
+ <td><i>validation.pem</i><td>
79
+ </tr>
80
+ <tr>
81
+ <td><b><i>chef_environment</i></b></td>
82
+ <td>Chef Environment used during bootstrap</td>
83
+ <td></td>
84
+ </tr>
85
+ <tr>
86
+ <td><b><i>chef_server_url</i></b></td>
87
+ <td>URL of the Chef Serer.</td>
88
+ <td></td>
89
+ </tr>
90
+ </table>
91
+
92
+ ## Installation
93
+
94
+ Add this line to your application's Gemfile:
95
+
96
+ gem 'hazetug'
97
+
98
+ And then execute:
99
+
100
+ $ bundle
101
+
102
+ Or install it yourself as:
103
+
104
+ $ gem install hazetug
105
+
106
+ ## Usage
107
+
108
+ ### Configuration file
109
+
110
+ Create *~/.hazetug* configuration file, with the content like:
111
+
112
+ ```yaml
113
+ default:
114
+ linode_api_key: YOUR_LINODE_API_KEY
115
+ linode_ssh_keys:
116
+ - ~/.ssh/linode.pem (Change with your path, it also might be missing)
117
+ digitalocean_api_key: DIGITALOCEAN_API_KEY
118
+ digitalocean_client_id: DIGITALOCEAN_CLIENT_ID
119
+ digitalocean_ssh_keys:
120
+ - ~/.ssh/digitalocean.pem (Change with your path)
121
+ ```
122
+
123
+ ### Tasks
124
+
125
+ Hazetug bootstrap task file is yaml file as well, it consists of two sections **global** and **bootstrap**. Global section sets default variables used by hazetug, each bootstraped host from bootstrap list sets variables specific to it thus redefining global defaults. Let's have a look at a sample task file:
126
+
127
+ ```
128
+ chef_server_url: 'https://mychefserver.uri'
129
+ chef_environment: staging
130
+ chef_version: 11.14.6
131
+ ruby_version: ruby-2.1.2
132
+ ssh_password: my-password-on-api-nodes
133
+ bootstrap:
134
+ - name: api-testbox-%rand(6)%
135
+ number: 2
136
+ location: london
137
+ flavor: 2gb
138
+ image: ubuntu-14.04-x64
139
+ run_list: ["role[api]"]
140
+ ```
141
+
142
+ From the example above we can see various variables used by hazetug they are common for all bootstrapped nodes, that's why it's reasonable to locate them in the global. However each variable has three layer hierarchy more details look into the [Variables Priority](README.md#variables-priority) section.
143
+
144
+ ### Variables priority
145
+
146
+ Hazetug uses 3-level priority for flexible variable choosing. Priority in the ascending order is the following: variable from the global section -> variable set via command option -> variable in the bootstrap list entity.
147
+ All variables are merged using this 3-level priority.
148
+
149
+
150
+ ### Command Line and Invocation
151
+
152
+ ### Bootstrap using knife
153
+
154
+ Help for linode compute is given bellow:
155
+
156
+ ```
157
+ NAME
158
+ knife - Bootstraps server using Knife
159
+
160
+ SYNOPSIS
161
+ hazetug.rb [global options] linode bootstrap knife [command options] task.yaml
162
+
163
+ COMMAND OPTIONS
164
+ -v, --variables=arg - Set variable or comma-seperated list of variables (var1_1=hello) (default: none)
165
+ -n, --number=arg - Set number of created nodes, value from yaml is honored (default: 1)
166
+ -c, --concurrency=arg - Set concurrency value, i.e. number of hosts bootstraped simultaneously (default: 1)
167
+ -b, --bootstrap=arg - Set path to knife bootstrap.erb file (default: bootstrap.erb)
168
+ ```
169
+
170
+ All variables are passed to the bootstrap template and are available using the hazetug hash like - `hazetug[:variable_name]`. Amongst variables described here in the options sections, hazetug also passes useful variables such as ***compute_name***, ***public_ip_address***, ***private_ip_address*** if those are available.
171
+
172
+ #### Examples
173
+
174
+ * Provisioning and bootstrapping 5 nodes, each 3 of them will be processed simultaneously:
175
+
176
+ `hazetug digitalocean bootstrap knife -n 5 -c 3 -b api.erb api-task.yaml`
177
+
178
+ * Redefining validation_key and chef_version:
179
+
180
+ `hazetug digitalocean bootstrap knife -v validation_key=/tmp/validation.pem,chef_version=11.12.4 api.erb api-task.yaml`
181
+
182
+ ## Contributing
183
+
184
+ 1. Fork it ( http://github.com/dennybaa/hazetug/fork )
185
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
186
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
187
+ 4. Push to the branch (`git push origin my-new-feature`)
188
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/hazetug.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hazetug/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "hazetug"
8
+ spec.version = Hazetug::VERSION
9
+ spec.authors = ["Denis Barishev"]
10
+ spec.email = ["denz@twiket.com"]
11
+ spec.summary = %q{Cloud provisoner tool}
12
+ spec.description = %q{Hazetug uses fog cloud library and he}
13
+ spec.homepage = "https://github.com/dennybaa/hazetug"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.5"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_dependency "psych"
24
+ spec.add_dependency "fog"
25
+ spec.add_dependency "chef", ">= 11.10.0"
26
+ spec.add_dependency "gli"
27
+ spec.add_dependency "agent"
28
+ end
@@ -0,0 +1,30 @@
1
+ require 'hazetug/ui'
2
+
3
+ class Hazetug
4
+ class CLI
5
+ class Action
6
+ attr_reader :data
7
+
8
+ def ui
9
+ Hazetug::UI.instance
10
+ end
11
+
12
+ def pass(hash={})
13
+ @data = hash.dup
14
+ self
15
+ end
16
+
17
+ class << self
18
+ def inherited(child)
19
+ action = child.name.split('::').last.downcase.to_sym
20
+ @actions ||= {}
21
+ @actions[action] = child
22
+ end
23
+
24
+ def [](value)
25
+ @actions[value]
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,88 @@
1
+ require 'agent'
2
+ require 'hazetug/cli/action'
3
+ require 'hazetug/tug'
4
+ require 'hazetug/task'
5
+
6
+
7
+ class Hazetug
8
+ class CLI
9
+ class Bootstrap < Action
10
+ def execute
11
+ concurrency = data[:opts][:concurrency].to_i || 1
12
+ queue = channel!(Object, concurrency)
13
+ waitgroup = Agent::WaitGroup.new
14
+ bootstrap_list do |haze, tug|
15
+ queue << nil; waitgroup.add(1)
16
+ block = method(:provision_and_bootstrap).to_proc
17
+ go!(haze, tug, queue, waitgroup, &block)
18
+ end
19
+ waitgroup.wait
20
+ end
21
+
22
+ def task
23
+ yaml_task = data[:args].shift
24
+ @task ||= Hazetug::Task.load_from_file(yaml_task)
25
+ end
26
+
27
+ def provision_and_bootstrap(haze, tug, channel, waitgroup)
28
+ haze.provision
29
+ tug.bootstrap({
30
+ args: data[:args],
31
+ opts: data[:opts],
32
+ gopts: data[:gopts]
33
+ })
34
+ rescue
35
+ # Exeception will be lost, since we run inside goproc,
36
+ # ie. as soon as waitgroup is empty all process exit.
37
+ puts $!.inspect
38
+ puts $@
39
+ ensure
40
+ waitgroup.done
41
+ channel.receive
42
+ end
43
+
44
+ def bootstrap_list(&block)
45
+ return if block.nil?
46
+ task.hosts_to_bootstrap(env_split) do |conf|
47
+ num = conf[:number] || data[:opts][:number].to_i || 1
48
+ if convert_rand_name(conf[:name]) == conf[:name] && num > 1
49
+ ui.fatal "Can't bootstrap several hosts with the same name"
50
+ raise ArgumentError, "%rand(x)% expected"
51
+ end
52
+ (1..num).each do
53
+ newconf = conf.dup
54
+ newconf[:name] = convert_rand_name(conf[:name])
55
+ haze = Hazetug::Haze[data[:compute_name]].new(newconf)
56
+ # Ensure a dynamic password loaded back from haze
57
+ if haze.config[:ssh_password]
58
+ newconf[:ssh_password] = haze.config[:ssh_password]
59
+ end
60
+ tug = Hazetug::Tug[data[:tug_name]].new(newconf, haze)
61
+ block.call(haze, tug)
62
+ end
63
+ end
64
+ end
65
+
66
+ def env_split
67
+ @env_split ||= begin
68
+ env = {}
69
+ arr = data[:opts][:env]
70
+ if arr
71
+ arr.each do |eq|
72
+ k, v = eq.split('=')
73
+ env[k] = v
74
+ end
75
+ end
76
+ env
77
+ end
78
+ end
79
+
80
+ def convert_rand_name(name)
81
+ name.sub(/%rand\((\d+)\)%/) do |m|
82
+ rand(36**$1.to_i).to_s(36)
83
+ end
84
+ end
85
+
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,74 @@
1
+ require 'gli'
2
+ require 'hazetug/cli/bootstrap'
3
+
4
+ class Hazetug
5
+ class CLI
6
+ include GLI::App
7
+
8
+ def run(args)
9
+ define_cli
10
+ super
11
+ end
12
+
13
+ private
14
+
15
+ def define_cli
16
+ sort_help :manually
17
+ switch [:v, :verbose]
18
+
19
+ {
20
+ digital_ocean: 'DigitalOcean compute',
21
+ linode: 'Linode Compute'
22
+ }.each do |compute, compute_desc|
23
+ desc compute_desc
24
+ default_command :linode
25
+
26
+ command compute.to_s.gsub(/_/,'') do |compute_cmd|
27
+
28
+ compute_cmd.desc 'Provisions and bootstraps server'
29
+ compute_cmd.command :bootstrap do |op|
30
+
31
+ op.desc 'Bootstraps server using Knife'
32
+ op.arg_name 'task.yaml'
33
+ op.command :knife do |tug|
34
+ tug.arg_name nil
35
+
36
+ tug.flag [:v, :variables], :must_match => Array,
37
+ :desc => 'Set variable or comma-seperated list of variables (var1_1=hello)'
38
+
39
+ tug.flag [:n, :number], :default_value => 1,
40
+ :desc => 'Set number of created nodes, value from yaml is honored'
41
+
42
+ tug.flag [:c, :concurrency], :default_value => 1,
43
+ :desc => 'Set concurrency value, i.e. number of hosts bootstraped simultaneously'
44
+
45
+ tug.flag [:b, :bootstrap], :default_value => 'bootstrap.erb',
46
+ :desc => 'Set path to knife bootstrap.erb file'
47
+
48
+ tug.action do |gopts, opts, args|
49
+
50
+ if args.empty?
51
+ commands[:help].execute({},{},tug.name_for_help)
52
+ exit 0
53
+ end
54
+
55
+ act = CLI::Action[:bootstrap].new
56
+ act.pass(
57
+ tug_name: :knife,
58
+ compute_name: compute,
59
+ cli: tug,
60
+ gopts: gopts,
61
+ opts: opts,
62
+ args: args
63
+ ).execute
64
+ end
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+
73
+ end
74
+ end
@@ -0,0 +1,80 @@
1
+ require 'fog/core'
2
+ require 'fog/xml'
3
+ require 'fog/json'
4
+ require 'fog/core/parser'
5
+
6
+ class Hazetug
7
+ class Compute
8
+ autoload :Linode, 'fog/linode'
9
+ autoload :DigitalOcean, 'fog/digitalocean'
10
+
11
+ module CloudMixin
12
+ CLOUD_MODELS = [
13
+ :locations,
14
+ :flavors,
15
+ :images
16
+ ]
17
+
18
+ def cloud?
19
+ true
20
+ end
21
+
22
+ def cloud_models
23
+ Hazetug::Compute::CloudMixin::CLOUD_MODELS
24
+ end
25
+
26
+ CLOUD_MODELS.each do |method_name|
27
+ define_method(method_name) do
28
+ collection = self.class.const_get(:CollectionMap) || {}
29
+ map_method = collection[method_name] ? collection[method_name] : method_name
30
+ fog.send(map_method)
31
+ end
32
+ end
33
+ end
34
+
35
+ class Base
36
+ def initialize(*args)
37
+ klass = self.class.name.split('::').last
38
+ @fog = Fog::Compute.const_get(klass).new(*args)
39
+ end
40
+
41
+ def method_missing(method_name, *args, &block)
42
+ # cache all missing methods calls
43
+ self.class.module_eval <<-EOS, __FILE__, __LINE__
44
+ def #{method_name}(*args, &block)
45
+ fog.#{method_name}(*args, &block)
46
+ end
47
+ EOS
48
+ fog.send(method_name, *args, &block)
49
+ end
50
+
51
+ class << self
52
+ def collection_map(mapping)
53
+ @collection_map = mapping
54
+ unless const_defined?(:CollectionMap)
55
+ module_eval "CollectionMap = @collection_map"
56
+ end
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ attr_reader :fog
63
+ end
64
+
65
+ class Linode < Base
66
+ include CloudMixin
67
+ collection_map({
68
+ :locations => :data_centers
69
+ })
70
+ end
71
+
72
+ class DigitalOcean < Base
73
+ include CloudMixin
74
+ collection_map({
75
+ :locations => :regions
76
+ })
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,26 @@
1
+ require 'psych'
2
+
3
+ class Hazetug
4
+ class Config
5
+ PATH = File.expand_path("~/.hazetug")
6
+
7
+ class << self
8
+ def load
9
+ Fog.credentials_path = PATH
10
+ @credentials ||= begin
11
+ if File.exist?(PATH)
12
+ Psych.load_file(PATH)[Fog.credential.to_s]
13
+ else
14
+ {}
15
+ end
16
+ end
17
+ end
18
+
19
+ def [](key)
20
+ @credentials[key]
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ Hazetug::Config.load
@@ -0,0 +1,71 @@
1
+ class Hazetug
2
+ class Haze
3
+ module CloudServer
4
+
5
+ def provision_server
6
+ ui.info "[#{compute_name}] creating server #{config[:name]}"
7
+ @server = compute.servers.create(server_args)
8
+ server.wait_for { ready? }
9
+ ui.info "[#{compute_name}] server #{config[:name]} created, ip: #{server.ssh_ip_address}"
10
+ end
11
+
12
+ def wait_for_ssh
13
+ ssh_options = {}
14
+ ssh_opts = Hazetug::Tug.ssh_options_from(config)
15
+ ssh_options[:password] = ssh_opts[:ssh_password]
16
+ ssh_options[:paranoid] = ssh_opts[:host_key_verify] || false
17
+ ssh_options[:keys] = ssh_opts[:ssh_keys] || Hazetug.ssh_keys(compute_name)
18
+ server.username = ssh_opts[:ssh_user]
19
+ server.ssh_port = ssh_opts[:ssh_port]
20
+ server.ssh_options = ssh_options
21
+ ui.info "[#{compute_name}] waiting for active ssh on #{server.ssh_ip_address}"
22
+ server.wait_for(30) { sshable? }
23
+ rescue Fog::Errors::TimeoutError
24
+ ui.error "[#{compute_name}] ssh failed to #{config[:name]}, ip: #{server.ssh_ip_address}"
25
+ end
26
+
27
+ def lookup(model, *args)
28
+ collection_method = model.to_s + 's'
29
+ compare_method = "compare_#{model}?"
30
+ found = compute.send(collection_method).select do |o|
31
+ self.send(compare_method, o, *args)
32
+ end
33
+ if found.size > 1
34
+ ui.error "More than one #{model} found for #{config[model]}"
35
+ elsif found.empty?
36
+ ui.fatal "#{model} not found for #{config[model]}"
37
+ raise ArgumentError, "Wrong argument #{config[model]}"
38
+ end
39
+ found.first
40
+ end
41
+
42
+ def memory_in_megabytes(string)
43
+ mult = 1
44
+ mult = 1024 if string.match(/gb$/i)
45
+ string.to_i * mult
46
+ end
47
+
48
+ def image_from_string(string)
49
+ string.downcase.sub(/\s*lts\s*/, '').
50
+ sub(RE_BITS, '').
51
+ sub(/\s+$/, '').
52
+ gsub(/ /, '-')
53
+ end
54
+
55
+ # Bits from string, when can't be fetched default is 64
56
+ def bits_from_string(string)
57
+ m = string.match(Haze::RE_BITS)
58
+ m ? m.captures.compact.first.to_s.to_i : 64
59
+ end
60
+
61
+ def server_args
62
+ @server_args ||= create_server_args
63
+ end
64
+
65
+ def create_server_args
66
+ raise NotImplementedError, "#create_server_arguments not implemented"
67
+ end
68
+
69
+ end
70
+ end
71
+ end