vmreverter 0.0.2

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,187 @@
1
+ # Apache Licensed - (github/puppetlabs) ripped from puppet_acceptance. ** See Legal notes
2
+ # Changes include namespace swaps, and refactoring
3
+
4
+ begin
5
+ require 'Vmreverter/logger'
6
+ rescue LoadError
7
+ require File.expand_path(File.join(File.dirname(__FILE__), '..', 'logger.rb'))
8
+ end
9
+
10
+ class VsphereHelper
11
+
12
+ #Class methods
13
+ def self.load_config authfile
14
+ vsphere_credentials = {}
15
+
16
+ if File.exists?(authfile)
17
+ vInfo = YAML.load_file(authfile)
18
+ elsif File.exists?( File.join(ENV['HOME'], '.fog') )
19
+ vInfo = YAML.load_file( File.join(ENV['HOME'], '.fog') )
20
+ else
21
+ report_and_raise(@logger, RuntimeError.new("Couldn't authentication for vSphere in auth file !"), "VSphereHelper::load_config")
22
+ end
23
+
24
+ begin
25
+ vsphere_credentials[:server] = vInfo[:default][:vsphere_server]
26
+ vsphere_credentials[:user] = vInfo[:default][:vsphere_username]
27
+ vsphere_credentials[:pass] = vInfo[:default][:vsphere_password]
28
+ rescue
29
+ report_and_raise(@logger, RuntimeError.new("Couldn't load authentication for vSphere in auth file !"), "VSphereHelper::load_config")
30
+ end
31
+
32
+ return vsphere_credentials
33
+ end
34
+
35
+ def initialize vInfo = {}
36
+ @logger = vInfo[:logger] || Vmreverter::Logger.new
37
+ begin
38
+ require 'rbvmomi'
39
+ rescue LoadError
40
+ raise "Unable to load RbVmomi, please ensure its installed"
41
+ end
42
+
43
+ # If you don’t have trusted SSL certificates installed on the host you’re connecting to,
44
+ #you’ll get an +OpenSSL::SSL::SSLError+ “certificate verify failed”.
45
+ #You can work around this by using the :insecure option to +RbVmomi::VIM.connect+.
46
+
47
+ @connection = RbVmomi::VIM.connect :host => vInfo[:server],
48
+ :user => vInfo[:user],
49
+ :password => vInfo[:pass],
50
+ :insecure => true
51
+ end
52
+ #Instance Methods
53
+ def find_snapshot vm, snapname
54
+ search_child_snaps vm.snapshot.rootSnapshotList, snapname
55
+ end
56
+
57
+ def search_child_snaps tree, snapname
58
+ snapshot = nil
59
+ tree.each do |child|
60
+ if child.name == snapname
61
+ snapshot ||= child.snapshot
62
+ else
63
+ snapshot ||= search_child_snaps child.childSnapshotList, snapname
64
+ end
65
+ end
66
+ snapshot
67
+ end
68
+
69
+ def find_customization name
70
+ csm = @connection.serviceContent.customizationSpecManager
71
+
72
+ begin
73
+ customizationSpec = csm.GetCustomizationSpec({:name => name}).spec
74
+ rescue
75
+ customizationSpec = nil
76
+ end
77
+
78
+ return customizationSpec
79
+ end
80
+
81
+ # an easier wrapper around the horrid PropertyCollector interface,
82
+ # necessary for searching VMs in all Datacenters that may be nested
83
+ # within folders of arbitrary depth
84
+ # returns a hash array of <name> => <VirtualMachine ManagedObjects>
85
+ def find_vms names, connection = @connection
86
+ names = names.is_a?(Array) ? names : [ names ]
87
+ containerView = get_base_vm_container_from connection
88
+ propertyCollector = connection.propertyCollector
89
+
90
+ objectSet = [{
91
+ :obj => containerView,
92
+ :skip => true,
93
+ :selectSet => [ RbVmomi::VIM::TraversalSpec.new({
94
+ :name => 'gettingTheVMs',
95
+ :path => 'view',
96
+ :skip => false,
97
+ :type => 'ContainerView'
98
+ }) ]
99
+ }]
100
+
101
+ propSet = [{
102
+ :pathSet => [ 'name' ],
103
+ :type => 'VirtualMachine'
104
+ }]
105
+
106
+ results = propertyCollector.RetrievePropertiesEx({
107
+ :specSet => [{
108
+ :objectSet => objectSet,
109
+ :propSet => propSet
110
+ }],
111
+ :options => { :maxObjects => nil }
112
+ })
113
+
114
+ vms = {}
115
+ results.objects.each do |result|
116
+ name = result.propSet.first.val
117
+ next unless names.include? name
118
+ vms[name] = result.obj
119
+ end
120
+
121
+ while results.token do
122
+ results = propertyCollector.ContinueRetrievePropertiesEx({:token => results.token})
123
+ results.objects.each do |result|
124
+ name = result.propSet.first.val
125
+ next unless names.include? name
126
+ vms[name] = result.obj
127
+ end
128
+ end
129
+ vms
130
+ end
131
+
132
+ def find_datastore datastorename
133
+ datacenter = @connection.serviceInstance.find_datacenter
134
+ datacenter.find_datastore(datastorename)
135
+ end
136
+
137
+ def find_folder foldername
138
+ datacenter = @connection.serviceInstance.find_datacenter
139
+ base = datacenter.vmFolder
140
+ folders = foldername.split('/')
141
+ folders.each do |folder|
142
+ case base
143
+ when RbVmomi::VIM::Folder
144
+ base = base.childEntity.find { |f| f.name == folder }
145
+ else
146
+ abort "Unexpected object type encountered (#{base.class}) while finding folder"
147
+ end
148
+ end
149
+
150
+ base
151
+ end
152
+
153
+ def find_pool poolname
154
+ datacenter = @connection.serviceInstance.find_datacenter
155
+ base = datacenter.hostFolder
156
+ pools = poolname.split('/')
157
+ pools.each do |pool|
158
+ case base
159
+ when RbVmomi::VIM::Folder
160
+ base = base.childEntity.find { |f| f.name == pool }
161
+ when RbVmomi::VIM::ClusterComputeResource
162
+ base = base.resourcePool.resourcePool.find { |f| f.name == pool }
163
+ when RbVmomi::VIM::ResourcePool
164
+ base = base.resourcePool.find { |f| f.name == pool }
165
+ else
166
+ abort "Unexpected object type encountered (#{base.class}) while finding resource pool"
167
+ end
168
+ end
169
+
170
+ base = base.resourcePool unless base.is_a?(RbVmomi::VIM::ResourcePool) and base.respond_to?(:resourcePool)
171
+ base
172
+ end
173
+
174
+ def get_base_vm_container_from connection
175
+ viewManager = connection.serviceContent.viewManager
176
+ viewManager.CreateContainerView({
177
+ :container => connection.serviceContent.rootFolder,
178
+ :recursive => true,
179
+ :type => [ 'VirtualMachine' ]
180
+ })
181
+ end
182
+
183
+ def close_connection
184
+ @connection.close
185
+ end
186
+ end
187
+
@@ -0,0 +1,34 @@
1
+ # Apache Licensed - (github/puppetlabs) ripped from puppet_acceptance. ** See Legal notes
2
+ # Changes include namespace swaps, method removal, method additions, and complete code refactoring
3
+
4
+ module Vmreverter
5
+
6
+ # Factory Pattern - Class to generate the correct hypervisor object, given type
7
+ class Hypervisor
8
+
9
+ def configure(hosts)
10
+ @logger.debug "No post-provisioning configuration necessary for #{self.class.name} boxes"
11
+ end
12
+
13
+ def self.register type, hosts_to_provision, options, config
14
+ @logger = options[:logger]
15
+ @logger.notify("Hypervisor found some #{type} boxes to hook")
16
+ case type.downcase
17
+ when /vsphere/
18
+ return Vmreverter::Vsphere.new hosts_to_provision, options, config
19
+ when /aws/
20
+ return Vmreverter::AWS.new hosts_to_provision, options, config
21
+ else
22
+ report_and_raise(@logger, RuntimeError.new("Missing Class for hypervisor invocation: (#{type})"), "Hypervisor::register")
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ %w( vsphere_helper aws vsphere ).each do |lib|
29
+ begin
30
+ require "hypervisor/#{lib}"
31
+ rescue LoadError
32
+ require File.expand_path(File.join(File.dirname(__FILE__), "hypervisor", lib))
33
+ end
34
+ end
@@ -0,0 +1,170 @@
1
+ # Apache Licensed - (github/puppetlabs) ripped from puppet_acceptance. ** See Legal notes
2
+ # Changes include namespace swaps, method removal, method additions, and complete code refactoring
3
+
4
+ module Vmreverter
5
+ class Logger
6
+ NORMAL = "\e[00;00m"
7
+ BRIGHT_NORMAL = "\e[00;01m"
8
+ BLACK = "\e[00;30m"
9
+ RED = "\e[00;31m"
10
+ GREEN = "\e[00;32m"
11
+ YELLOW = "\e[00;33m"
12
+ BLUE = "\e[00;34m"
13
+ MAGENTA = "\e[00;35m"
14
+ CYAN = "\e[00;36m"
15
+ WHITE = "\e[00;37m"
16
+ GREY = "\e[01;30m"
17
+ BRIGHT_RED = "\e[01;31m"
18
+ BRIGHT_GREEN = "\e[01;32m"
19
+ BRIGHT_YELLOW = "\e[01;33m"
20
+ BRIGHT_BLUE = "\e[01;34m"
21
+ BRIGHT_MAGENTA = "\e[01;35m"
22
+ BRIGHT_CYAN = "\e[01;36m"
23
+ BRIGHT_WHITE = "\e[01;37m"
24
+
25
+ LOG_LEVELS = {
26
+ :debug => 1,
27
+ :warn => 2,
28
+ :normal => 3,
29
+ :info => 4
30
+ }
31
+
32
+ attr_accessor :color, :log_level, :destinations
33
+
34
+ def initialize(*args)
35
+ options = args.last.is_a?(Hash) ? args.pop : {}
36
+ @color = options[:color]
37
+ @log_level = options[:debug] ? :debug : :normal
38
+ @destinations = []
39
+
40
+ dests = args
41
+ dests << STDOUT unless options[:quiet]
42
+ dests.uniq!
43
+ dests.each {|dest| add_destination(dest)}
44
+ end
45
+
46
+ def add_destination(dest)
47
+ case dest
48
+ when IO
49
+ @destinations << dest
50
+ when String
51
+ @destinations << File.open(dest, 'w')
52
+ else
53
+ raise "Unsuitable log destination #{dest.inspect}"
54
+ end
55
+ end
56
+
57
+ def remove_destination(dest)
58
+ case dest
59
+ when IO
60
+ @destinations.delete(dest)
61
+ when String
62
+ @destinations.delete_if {|d| d.respond_to?(:path) and d.path == dest}
63
+ else
64
+ raise "Unsuitable log destination #{dest.inspect}"
65
+ end
66
+ end
67
+
68
+ def is_debug?
69
+ LOG_LEVELS[@log_level] <= LOG_LEVELS[:debug]
70
+ end
71
+
72
+ def is_warn?
73
+ LOG_LEVELS[@log_level] <= LOG_LEVELS[:warn]
74
+ end
75
+
76
+ def host_output *args
77
+ return unless is_debug?
78
+ strings = strip_colors_from args
79
+ string = strings.join
80
+ optionally_color GREY, string, false
81
+ end
82
+
83
+ def debug *args
84
+ return unless is_debug?
85
+ optionally_color WHITE, args
86
+ end
87
+
88
+ def warn *args
89
+ return unless is_warn?
90
+ strings = args.map {|msg| "Warning: #{msg}" }
91
+ optionally_color YELLOW, strings
92
+ end
93
+
94
+ def success *args
95
+ optionally_color GREEN, args
96
+ end
97
+
98
+ def notify *args
99
+ optionally_color BRIGHT_WHITE, args
100
+ end
101
+
102
+ def error *args
103
+ optionally_color BRIGHT_RED, args
104
+ end
105
+
106
+ def strip_colors_from lines
107
+ Array(lines).map do |line|
108
+ line.gsub /\e\[(\d+;)?\d+m/, ''
109
+ end
110
+ end
111
+
112
+ def optionally_color color_code, msg, add_newline = true
113
+ print_statement = add_newline ? :puts : :print
114
+ @destinations.each do |to|
115
+ to.print color_code if @color
116
+ to.send print_statement, msg
117
+ to.print NORMAL if @color
118
+ end
119
+ end
120
+
121
+ # utility method to get the current call stack and format it
122
+ # to a human-readable string (which some IDEs/editors
123
+ # will recognize as links to the line numbers in the trace)
124
+ def pretty_backtrace backtrace = caller(1)
125
+ trace = purge_harness_files_from( Array( backtrace ) )
126
+ expand_symlinks( trace ).join "\n"
127
+ end
128
+
129
+ private
130
+ def expand_symlinks backtrace
131
+ backtrace.collect do |line|
132
+ file_path, line_num = line.split( ":" )
133
+ expanded_path = expand_symlink File.expand_path( file_path )
134
+ expanded_path.to_s + ":" + line_num.to_s
135
+ end
136
+ end
137
+
138
+ def purge_harness_files_from backtrace
139
+ mostly_purged = backtrace.reject do |line|
140
+ # LOADED_FEATURES is an array of anything `require`d, i.e. everything
141
+ # but the test in question
142
+ $LOADED_FEATURES.any? do |require_path|
143
+ line.include? require_path
144
+ end
145
+ end
146
+
147
+ # And remove lines that contain our program name in them
148
+ completely_purged = mostly_purged.reject {|line| line.include? $0 }
149
+ end
150
+
151
+ # utility method that takes a path as input, checks each component
152
+ # of the path to see if it is a symlink, and expands
153
+ # it if it is. returns the expanded path.
154
+ def expand_symlink file_path
155
+ file_path.split( "/" ).inject do |full_path, next_dir|
156
+ next_path = full_path + "/" + next_dir
157
+ if File.symlink? next_path
158
+ link = File.readlink next_path
159
+ next_path =
160
+ case link
161
+ when /^\// then link
162
+ else
163
+ File.expand_path( full_path + "/" + link )
164
+ end
165
+ end
166
+ next_path
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,104 @@
1
+ module Vmreverter
2
+ class Options
3
+
4
+ def self.options
5
+ return @options
6
+ end
7
+
8
+
9
+ def self.parse_args
10
+ return @options if @options
11
+
12
+ @no_args = ARGV.empty? ? true : false
13
+
14
+ @defaults = {}
15
+ @options = {}
16
+ @options_from_file = {}
17
+
18
+ optparse = OptionParser.new do|opts|
19
+ # Set a banner
20
+ opts.banner = "Usage: #{File.basename($0)} [options...]"
21
+
22
+ @defaults[:auth] = File.join(ENV['HOME'], '.fog')
23
+ opts.on '-a', '--auth FILE',
24
+ 'Use authentication FILE',
25
+ "Default: #{@defaults[:auth]}" do |file|
26
+ @options[:auth] = file
27
+ end
28
+
29
+ @defaults[:config] = nil
30
+ opts.on '-c', '--config FILE',
31
+ 'Use configuration FILE',
32
+ "Default: #{@defaults[:config]}" do |file|
33
+ @options[:config] = file
34
+ end
35
+
36
+ @defaults[:options_file] = nil
37
+ opts.on '-o', '--options-file FILE',
38
+ 'Read options from FILE',
39
+ 'This should evaluate to a ruby hash.',
40
+ 'CLI optons are given precedence.' do |file|
41
+ @options_from_file = parse_options_file file
42
+ end
43
+
44
+ @defaults[:quiet] = false
45
+ opts.on '-q', '--[no-]quiet',
46
+ 'Do not log output to STDOUT',
47
+ '(default: false)' do |bool|
48
+ @options[:quiet] = bool
49
+ end
50
+
51
+ @defaults[:color] = true
52
+ opts.on '--[no-]color',
53
+ 'Do not display color in log output',
54
+ '(default: true)' do |bool|
55
+ @options[:color] = bool
56
+ end
57
+
58
+ @defaults[:debug] = false
59
+ opts.on '--[no-]debug',
60
+ 'Enable full debugging',
61
+ '(default: false)' do |bool|
62
+ @options[:debug] = bool
63
+ end
64
+
65
+
66
+ opts.on_tail("-h","--help","Display this screen") do
67
+ puts opts
68
+ exit
69
+ end
70
+ end
71
+
72
+ optparse.parse!
73
+
74
+ # We have use the @no_args var because OptParse consumes ARGV as it parses
75
+ # so we have to check the value of ARGV at the begining of the method,
76
+ # let the options be set, then output usage.
77
+ puts optparse if @no_args
78
+
79
+ # merge in the options that we read from the file
80
+ @options = @options_from_file.merge(@options)
81
+ # merge in defaults
82
+ @options = @defaults.merge(@options)
83
+
84
+ @options
85
+ end
86
+
87
+ def self.parse_options_file(options_file_path)
88
+ options_file_path = File.expand_path(options_file_path)
89
+ unless File.exists?(options_file_path)
90
+ raise ArgumentError, "Specified options file '#{options_file_path}' does not exist!"
91
+ end
92
+ # This eval will allow the specified options file to have access to our
93
+ # scope. It is important that the variable 'options_file_path' is
94
+ # accessible, because some existing options files (e.g. puppetdb) rely on
95
+ # that variable to determine their own location (for use in 'require's, etc.)
96
+ result = eval(File.read(options_file_path))
97
+ unless result.is_a? Hash
98
+ raise ArgumentError, "Options file '#{options_file_path}' must return a hash!"
99
+ end
100
+
101
+ result
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,20 @@
1
+ # Apache Licensed - (github/puppetlabs) ripped from puppet_acceptance. ** See Legal notes
2
+ # Changes include namespace swaps, method removal, method additions, and complete code refactoring
3
+
4
+ module Vmreverter
5
+ module Shared
6
+ module ErrorHandler
7
+
8
+ def report_and_raise(logger, e, msg)
9
+ logger.error "Failed: errored in #{msg}"
10
+ logger.error(e.inspect)
11
+ bt = e.backtrace
12
+ logger.pretty_backtrace(bt).each_line do |line|
13
+ logger.error(line)
14
+ end
15
+ raise e
16
+ end
17
+
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,13 @@
1
+
2
+ begin
3
+ require "vmreverter/shared/error_handler"
4
+ rescue LoadError
5
+ require File.expand_path(File.join(File.dirname(__FILE__), 'shared', file))
6
+ end
7
+
8
+ module Vmreverter
9
+ module Shared
10
+ include Vmreverter::Shared::ErrorHandler
11
+ end
12
+ end
13
+ include Vmreverter::Shared
@@ -0,0 +1,3 @@
1
+ module Vmreverter
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,60 @@
1
+ %w(hypervisor).each do |lib|
2
+ begin
3
+ require "vmreverter/#{lib}"
4
+ rescue LoadError
5
+ require File.expand_path(File.join(File.dirname(__FILE__), lib))
6
+ end
7
+ end
8
+
9
+ module Vmreverter
10
+
11
+ HYPERVISOR_TYPES = ['vsphere', 'aws']
12
+
13
+ #Collection using Proxy Pattern - Proxy normalized access to specific Hypervisors such as VSphere through creation by the Hypervisor Factory
14
+ class VMManager
15
+
16
+ attr_accessor :hypervisor_collection
17
+
18
+ def initialize(config, options, logger)
19
+ @logger = logger
20
+ @options = options
21
+ @hosts = []
22
+ @config = config
23
+ @hypervisor_collection = {}
24
+ @virtual_machines = {}
25
+
26
+ @config['HOSTS'].each_key do |name|
27
+ hypervisor = @config['HOSTS'][name]['hypervisor']
28
+ @logger.debug "Hypervisor for #{name} is #{hypervisor}"
29
+ @virtual_machines[hypervisor] = [] unless @virtual_machines[hypervisor]
30
+ @virtual_machines[hypervisor] << name
31
+ end
32
+
33
+ ## Data Model Looks like
34
+ # @virtual_machines.inspect
35
+ # {"vsphere" => ["test_server01","test_server02"], "blimpy" => ["aws_test_server01","aws_test_server01"]}
36
+
37
+ @virtual_machines.each do |type, names|
38
+ @hypervisor_collection[type] = Vmreverter::Hypervisor.register(type, names, @options, @config)
39
+ end
40
+
41
+ #return instance created
42
+ return self
43
+ end
44
+
45
+ def invoke
46
+ @hypervisor_collection.each do |hypervisor_type, hypervisor_instance|
47
+ @logger.notify("Invoking #{hypervisor_type} hosts")
48
+ hypervisor_instance.invoke
49
+ end
50
+ end
51
+
52
+ def close_connection
53
+ @hypervisor_collection.each do |hypervisor_type, hypervisor_instance|
54
+ @logger.notify("Disconnecting from #{hypervisor_type}")
55
+ hypervisor_instance.close_connection
56
+ end
57
+ end
58
+
59
+ end
60
+ end
data/lib/vmreverter.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'optparse'
2
+ require 'open-uri'
3
+ require 'yaml'
4
+
5
+ require 'rubygems' unless defined?(Gem)
6
+
7
+ require 'pry'
8
+ require 'pry-nav'
9
+ module Vmreverter
10
+ $:.unshift(File.dirname(__FILE__))
11
+
12
+ # logger
13
+ require 'vmreverter/logger'
14
+
15
+ # Shared methods and helpers
16
+ require 'vmreverter/shared'
17
+
18
+ # hypervisor methods and helpers
19
+ require 'vmreverter/hypervisor'
20
+
21
+ %w( options vmmanager config_tester cli ).each do |lib|
22
+ begin
23
+ require "vmreverter/#{lib}"
24
+ rescue LoadError
25
+ require File.expand_path(File.join(File.dirname(__FILE__), 'vmreverter', lib))
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,38 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require File.expand_path('../lib/vmreverter/version', __FILE__)
4
+
5
+ require 'rbconfig'
6
+ ruby_conf = defined?(RbConfig) ? RbConfig::CONFIG : Config::CONFIG
7
+ less_than_one_nine = ruby_conf['MAJOR'].to_i == 1 && ruby_conf['MINOR'].to_i < 9
8
+
9
+ Gem::Specification.new do |s|
10
+ s.name = "vmreverter"
11
+ s.authors = ["shadowbq"]
12
+ s.email = ["shadowbq@gmail.com"]
13
+ s.homepage = "https://github.com/shadowbq/vmreverter"
14
+ s.summary = %q{Revert VM to previous snapshots }
15
+ s.description = s.summary
16
+
17
+ s.files = `git ls-files`.split($\)
18
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
19
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ s.version = Vmreverter::VERSION
22
+ s.licenses = ["BSD3", "APACHE2"]
23
+
24
+ s.add_development_dependency 'bundler', '~> 1.0'
25
+
26
+ # Testing dependencies
27
+ s.add_development_dependency 'rake'
28
+ s.add_development_dependency 'pry'
29
+ s.add_development_dependency 'pry-nav'
30
+
31
+
32
+ # Run time dependencies
33
+ s.add_runtime_dependency 'json'
34
+ # Fix bug with interdepency of rbnmomi and nokogiri https://github.com/vmware/rbvmomi/issues/31
35
+ s.add_runtime_dependency 'nokogiri', '1.5.5'
36
+ s.add_runtime_dependency 'rbvmomi'
37
+ s.add_runtime_dependency 'blimpy'
38
+ end