foreman_snapshot_management 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +619 -0
- data/README.md +15 -0
- data/Rakefile +47 -0
- data/app/controllers/foreman_snapshot_management/snapshots_controller.rb +117 -0
- data/app/helpers/foreman_snapshot_management/snapshot_helper.rb +7 -0
- data/app/helpers/foreman_snapshot_management/snapshotadministration_helper.rb +72 -0
- data/app/models/foreman_snapshot_management/snapshot.rb +73 -0
- data/app/models/foreman_snapshot_management/vmware_extensions.rb +40 -0
- data/app/overrides/hosts/add_tab_to_host_overview.rb +21 -0
- data/app/views/foreman_snapshot_management/snapshots/_index.html.erb +57 -0
- data/config/routes.rb +11 -0
- data/lib/foreman_snapshot_management.rb +4 -0
- data/lib/foreman_snapshot_management/engine.rb +66 -0
- data/lib/foreman_snapshot_management/version.rb +3 -0
- data/lib/tasks/foreman_snapshot_management_tasks.rake +35 -0
- data/locale/Makefile +60 -0
- data/locale/en/foreman_snapshot_management.po +19 -0
- data/locale/foreman_snapshot_management.pot +19 -0
- data/locale/gemspec.rb +2 -0
- data/test/test_plugin_helper.rb +2 -0
- data/test/unit/foreman_snapshot_management_test.rb +11 -0
- metadata +94 -0
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# ForemanSnapshotManagement
|
2
|
+
|
3
|
+
ForemanSnapshotManagement is a Foreman plugin to manage snapshots.
|
4
|
+
As Hypervisor VMware vSphere is supported.
|
5
|
+
|
6
|
+
## Features
|
7
|
+
|
8
|
+
- List existing snapshots of a virtual machine.
|
9
|
+
- Create a snapshot of a virtual machine.
|
10
|
+
- Revert existing virtual machine to a previously created snapshot.
|
11
|
+
- Remove existing snapshots of a virtual machine.
|
12
|
+
|
13
|
+
## Installation
|
14
|
+
|
15
|
+
See [How_to_Install_a_Plugin](http://projects.theforeman.org/projects/foreman/wiki/How_to_Install_a_Plugin) for how to install Foreman plugins
|
data/Rakefile
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'ForemanSnapshotManagement'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
|
24
|
+
|
25
|
+
Bundler::GemHelper.install_tasks
|
26
|
+
|
27
|
+
require 'rake/testtask'
|
28
|
+
|
29
|
+
Rake::TestTask.new(:test) do |t|
|
30
|
+
t.libs << 'lib'
|
31
|
+
t.libs << 'test'
|
32
|
+
t.pattern = 'test/**/*_test.rb'
|
33
|
+
t.verbose = false
|
34
|
+
end
|
35
|
+
|
36
|
+
task default: :test
|
37
|
+
|
38
|
+
begin
|
39
|
+
require 'rubocop/rake_task'
|
40
|
+
RuboCop::RakeTask.new
|
41
|
+
rescue => _
|
42
|
+
puts 'Rubocop not loaded.'
|
43
|
+
end
|
44
|
+
|
45
|
+
task :default do
|
46
|
+
Rake::Task['rubocop'].execute
|
47
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
module ForemanSnapshotManagement
|
2
|
+
class SnapshotsController < ApplicationController
|
3
|
+
before_action :find_host
|
4
|
+
before_action :enumerate_snapshots, only: [:index]
|
5
|
+
before_action :find_snapshot, only: %i[destroy revert update]
|
6
|
+
helper_method :xeditable?
|
7
|
+
|
8
|
+
def xeditable?(_object = nil)
|
9
|
+
true
|
10
|
+
end
|
11
|
+
|
12
|
+
def index
|
13
|
+
@new_snapshot = Snapshot.new(host: @host)
|
14
|
+
render partial: 'index'
|
15
|
+
end
|
16
|
+
|
17
|
+
# Create a Snapshot.
|
18
|
+
#
|
19
|
+
# This method creates a Snapshot with a given name and optional description.
|
20
|
+
def create
|
21
|
+
# Create snapshot
|
22
|
+
name = params['snapshot']['name']
|
23
|
+
description = params['snapshot']['description'] || ''
|
24
|
+
snapshot = Snapshot.new(host: @host, name: name, description: description)
|
25
|
+
begin
|
26
|
+
task = snapshot.create
|
27
|
+
|
28
|
+
# Check state of Snapshot creation
|
29
|
+
if task['task_state'] == 'success'
|
30
|
+
status = _('Snapshot successfully created')
|
31
|
+
else
|
32
|
+
status = _('Error occurred while creating snapshot: %s') % task['task_state']
|
33
|
+
end
|
34
|
+
|
35
|
+
# Redirect to specific Host page
|
36
|
+
redirect_to host_path(@host, anchor: 'snapshots'), flash: { notice: status }
|
37
|
+
rescue
|
38
|
+
logger.error 'Failed to take snapshot.'
|
39
|
+
status = _('Error occurred while creating snapshot.')
|
40
|
+
redirect_to host_path(@host, anchor: 'snapshots'), flash: { error: status }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Remove Snapshot
|
45
|
+
#
|
46
|
+
# This method removes a Snapshot from a given host.
|
47
|
+
def destroy
|
48
|
+
# Remove Snapshot
|
49
|
+
task = @snapshot.destroy
|
50
|
+
|
51
|
+
# Check state of Snapshot creation
|
52
|
+
if task['task_state'] == 'success'
|
53
|
+
status = 'Snapshot successfully deleted'
|
54
|
+
else
|
55
|
+
status = 'Error occurred while removing Snapshot: ' + task['task_state']
|
56
|
+
end
|
57
|
+
|
58
|
+
# Redirect to specific Host page
|
59
|
+
redirect_to host_path(@host, anchor: 'snapshots'), flash: { notice: status }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Revert Snapshot
|
63
|
+
#
|
64
|
+
# This method reverts a host to a given Snapshot.
|
65
|
+
def revert
|
66
|
+
# Revert Snapshot
|
67
|
+
task = @snapshot.revert
|
68
|
+
|
69
|
+
# Check state of Snapshot creation
|
70
|
+
if task['state'] == 'success'
|
71
|
+
status = _('VM successfully rolled back')
|
72
|
+
else
|
73
|
+
status = _('Error occurred while rolling back VM: %s') % task['state']
|
74
|
+
end
|
75
|
+
|
76
|
+
# Redirect to specific Host page
|
77
|
+
redirect_to host_path(@host, anchor: 'snapshots'), flash: { notice: status }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Update Snapshot
|
81
|
+
#
|
82
|
+
# This method renames a Snapshot from a given host.
|
83
|
+
def update
|
84
|
+
# Rename Snapshot
|
85
|
+
@snapshot.name = params['snapshot']['name'] if params['snapshot']['name']
|
86
|
+
@snapshot.description = params['snapshot']['description'] if params['snapshot']['description']
|
87
|
+
begin
|
88
|
+
task = @snapshot.save
|
89
|
+
render json: { name: @snapshot.name, description: @snapshot.description }
|
90
|
+
rescue
|
91
|
+
logger.error "Failed to update snapshot #{@snapshot.id}."
|
92
|
+
render json: { errors: _('Failed to update snapshot.') }, status: :unprocessable_entity
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
# Find Host
|
97
|
+
#
|
98
|
+
# This method is responsible that methods of the controller know the current host.
|
99
|
+
|
100
|
+
private
|
101
|
+
|
102
|
+
def find_host
|
103
|
+
@host = Host.find_by! name: params['host_id']
|
104
|
+
rescue => e
|
105
|
+
process_ajax_error e, 'Host not found!'
|
106
|
+
end
|
107
|
+
|
108
|
+
def enumerate_snapshots
|
109
|
+
# Hash of Snapshot
|
110
|
+
@snapshots = Snapshot.all_for_host @host
|
111
|
+
end
|
112
|
+
|
113
|
+
def find_snapshot
|
114
|
+
@snapshot = Snapshot.find_for_host @host, params['id']
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ForemanSnapshotManagement
|
2
|
+
module SnapshotadministrationHelper
|
3
|
+
end
|
4
|
+
end
|
5
|
+
|
6
|
+
module Fog
|
7
|
+
module Compute
|
8
|
+
class Vsphere
|
9
|
+
class Real
|
10
|
+
# Extends fog-vsphere gem for a remove Snapshot method.
|
11
|
+
def remove_snapshot(options = {})
|
12
|
+
raise ArgumentError, 'snapshot is a required parameter' unless options.key? 'snapshot'
|
13
|
+
raise ArgumentError, 'removeChildren is a required parameter' unless options.key? 'removeChildren'
|
14
|
+
|
15
|
+
unless Snapshot === options['snapshot']
|
16
|
+
raise ArgumentError, 'snapshot is a required parameter'
|
17
|
+
end
|
18
|
+
|
19
|
+
task = options['snapshot'].mo_ref.RemoveSnapshot_Task(
|
20
|
+
removeChildren: options['removeChildren']
|
21
|
+
)
|
22
|
+
task.wait_for_completion
|
23
|
+
|
24
|
+
{
|
25
|
+
'task_state' => task.info.state
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
# Extends fog-vsphere gem for a rename Snapshot method.
|
30
|
+
# TODO: Add info state
|
31
|
+
def rename_snapshot(options = {})
|
32
|
+
raise ArgumentError, 'snapshot is a required parameter' unless options.key? 'snapshot'
|
33
|
+
raise ArgumentError, 'name is a required parameter' unless options.key? 'name'
|
34
|
+
raise ArgumentError, 'description is a required parameter' unless options.key? 'description'
|
35
|
+
|
36
|
+
unless Snapshot === options['snapshot']
|
37
|
+
raise ArgumentError, 'snapshot is a required parameter'
|
38
|
+
end
|
39
|
+
|
40
|
+
task = options['snapshot'].mo_ref.RenameSnapshot(
|
41
|
+
name: options['name'],
|
42
|
+
description: options['description']
|
43
|
+
)
|
44
|
+
# task.wait_for_completion
|
45
|
+
|
46
|
+
# {
|
47
|
+
# 'task_state' => task.info.state
|
48
|
+
# }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
class Mock
|
52
|
+
def remove_snapshot(snapshot)
|
53
|
+
raise ArgumentError, 'snapshot is a required parameter' unless options.key? 'snapshot'
|
54
|
+
raise ArgumentError, 'removeChildren is a required parameter' unless options.key? 'removeChildren'
|
55
|
+
raise ArgumentError, 'snapshot is a required parameter' if snapshot.nil?
|
56
|
+
{
|
57
|
+
'task_state' => 'success'
|
58
|
+
}
|
59
|
+
end
|
60
|
+
|
61
|
+
def rename_snapshot(_snapshot)
|
62
|
+
raise ArgumentError, 'snapshot is a required parameter' unless options.key? 'snapshot'
|
63
|
+
raise ArgumentError, 'name is a required parameter' unless options.key? 'name'
|
64
|
+
raise ArgumentError, 'description is a required parameter' unless options.key? 'description'
|
65
|
+
{
|
66
|
+
'task_state' => 'success'
|
67
|
+
}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module ForemanSnapshotManagement
|
2
|
+
class Snapshot
|
3
|
+
extend ActiveModel::Callbacks
|
4
|
+
include ActiveModel::Conversion
|
5
|
+
include ActiveModel::Model
|
6
|
+
include ActiveModel::Dirty
|
7
|
+
|
8
|
+
define_model_callbacks :create, :save, :destroy, :revert
|
9
|
+
attr_accessor :id, :vmware_snapshot, :name, :description, :host_id
|
10
|
+
|
11
|
+
def self.add_snapshot_with_children(snapshots, host, vmware_snapshot)
|
12
|
+
snapshots[vmware_snapshot.ref] = new_from_vmware(host, vmware_snapshot)
|
13
|
+
vmware_snapshot.child_snapshots.each do |snap|
|
14
|
+
add_snapshot_with_children(snapshots, host, snap)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.all_for_host(host)
|
19
|
+
snapshots = {}
|
20
|
+
root_snapshot = host.compute_resource.get_snapshots(host.uuid).first
|
21
|
+
add_snapshot_with_children(snapshots, host, root_snapshot) if root_snapshot
|
22
|
+
snapshots
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.find_for_host(host, id)
|
26
|
+
all_for_host(host)[id]
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.new_from_vmware(host, vmware_snapshot)
|
30
|
+
new(host: host, id: vmware_snapshot.ref, vmware_snapshot: vmware_snapshot, name: vmware_snapshot.name, description: vmware_snapshot.description)
|
31
|
+
end
|
32
|
+
|
33
|
+
def persisted?
|
34
|
+
@id.present?
|
35
|
+
end
|
36
|
+
|
37
|
+
# host accessors
|
38
|
+
def host
|
39
|
+
Host.find(@host_id)
|
40
|
+
end
|
41
|
+
|
42
|
+
def host=(host)
|
43
|
+
@host_id = host.id
|
44
|
+
end
|
45
|
+
|
46
|
+
# crud
|
47
|
+
def create
|
48
|
+
run_callbacks(:create) do
|
49
|
+
host.compute_resource.create_snapshot(host.uuid, name, description)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def save
|
54
|
+
run_callbacks(:save) do
|
55
|
+
host.compute_resource.update_snapshot(vmware_snapshot, name, description)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def destroy
|
60
|
+
run_callbacks(:destroy) do
|
61
|
+
result = host.compute_resource.remove_snapshot(vmware_snapshot, false)
|
62
|
+
id = nil
|
63
|
+
result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def revert
|
68
|
+
run_callbacks(:revert) do
|
69
|
+
host.compute_resource.revert_snapshot(vmware_snapshot)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module ForemanSnapshotManagement
|
2
|
+
module VmwareExtensions
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
# Create a Snapshot.
|
6
|
+
#
|
7
|
+
# This method creates a Snapshot with a given name and optional description.
|
8
|
+
def create_snapshot(uuid, name, description)
|
9
|
+
client.vm_take_snapshot('instance_uuid' => uuid, 'name' => name, 'description' => description)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Remove Snapshot
|
13
|
+
#
|
14
|
+
# This method removes a Snapshot from a given host.
|
15
|
+
def remove_snapshot(snapshot, removeChildren)
|
16
|
+
client.remove_snapshot('snapshot' => snapshot, 'removeChildren' => removeChildren)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Revert Snapshot
|
20
|
+
#
|
21
|
+
# This method revert a host to a given Snapshot.
|
22
|
+
def revert_snapshot(snapshot)
|
23
|
+
client.revert_to_snapshot(snapshot)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Update Snapshot
|
27
|
+
#
|
28
|
+
# This method renames a Snapshot from a given host.
|
29
|
+
def update_snapshot(snapshot, name, description)
|
30
|
+
client.rename_snapshot('snapshot' => snapshot, 'name' => name, 'description' => description)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Get Snapshots
|
34
|
+
#
|
35
|
+
# This methods returns Snapshots from a given host.
|
36
|
+
def get_snapshots(server_id)
|
37
|
+
client.snapshots(server_id: server_id)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
tab = "<% if @host.provider == 'VMware' %>
|
2
|
+
<li><a href='#snapshots' data-toggle='tab'><%= _('Snapshots') %></a></li>
|
3
|
+
<% end %>"
|
4
|
+
|
5
|
+
tab_content = "<div id='snapshots' class='tab-pane'
|
6
|
+
data-ajax-url='<%= host_snapshots_path(host_id: @host)%>'
|
7
|
+
data-on-complete='onContentLoad'>
|
8
|
+
<%= spinner(_('Loading Parameters information ...')) %>
|
9
|
+
</div>"
|
10
|
+
|
11
|
+
# Add a Snapshots tab in the view of a host
|
12
|
+
Deface::Override.new(virtual_path: 'hosts/show',
|
13
|
+
name: 'add_host_snapshot_tab',
|
14
|
+
insert_bottom: 'ul',
|
15
|
+
text: tab)
|
16
|
+
|
17
|
+
# Load content of Snapshots tab
|
18
|
+
Deface::Override.new(virtual_path: 'hosts/show',
|
19
|
+
name: 'add_host_snapshots_tab_content',
|
20
|
+
insert_bottom: 'div#myTabContent',
|
21
|
+
text: tab_content)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
<%= form_for @new_snapshot, as: :snapshot, url: host_snapshots_path(@host), html: {class: ""} do |f| %>
|
2
|
+
<table class="<%= table_css_classes %>">
|
3
|
+
<thead>
|
4
|
+
<tr>
|
5
|
+
<th class="col-md-1"><%= _('Snapshot') %></th>
|
6
|
+
<th class="col-md-2"><%= _('Description') %></th>
|
7
|
+
<th class="col-md-1"><%= _('Action') %></th>
|
8
|
+
</tr>
|
9
|
+
</thead>
|
10
|
+
<tbody>
|
11
|
+
<tr>
|
12
|
+
<td>
|
13
|
+
<%= f.text_field :name, class: 'form-control' %>
|
14
|
+
</td>
|
15
|
+
<td>
|
16
|
+
<%= f.text_field :description, class: 'form-control' %>
|
17
|
+
</td>
|
18
|
+
<td>
|
19
|
+
<%= f.submit _('Create'), class: 'btn btn-success' %>
|
20
|
+
</td>
|
21
|
+
</tr>
|
22
|
+
<% @snapshots.each do |ref, snap| %>
|
23
|
+
<tr>
|
24
|
+
<td>
|
25
|
+
<%= edit_textfield snap, :name %>
|
26
|
+
</td>
|
27
|
+
<td>
|
28
|
+
<%= edit_textarea snap, :description %>
|
29
|
+
</td>
|
30
|
+
<td>
|
31
|
+
<%= action_buttons(
|
32
|
+
display_link_if_authorized(_('Rollback'), hash_for_revert_host_snapshot_path(host_id: @host, id: ref), method: :put, class: 'btn btn-primary', data: {confirm: _("Are you sure to revert this Snapshot?")}),
|
33
|
+
display_delete_if_authorized(hash_for_host_snapshot_path(host_id: @host, id: ref), data: {confirm: _("Are you sure to delete this Snapshot?")}),
|
34
|
+
) %>
|
35
|
+
</td>
|
36
|
+
</tr>
|
37
|
+
<% end %>
|
38
|
+
</tbody>
|
39
|
+
</table>
|
40
|
+
<% end %>
|
41
|
+
|
42
|
+
<script type="text/javascript">
|
43
|
+
//<![CDATA[
|
44
|
+
$(document).ready(function() {
|
45
|
+
$('.editable').editable({
|
46
|
+
params: {
|
47
|
+
authenticity_token: AUTH_TOKEN
|
48
|
+
},
|
49
|
+
error: function(response) {
|
50
|
+
return $.parseJSON(response.responseText).errors;
|
51
|
+
}
|
52
|
+
});
|
53
|
+
var hash = window.location.hash;
|
54
|
+
hash && $('ul.nav a[href="' + hash + '"]').tab('show');
|
55
|
+
});
|
56
|
+
//]]>
|
57
|
+
</script>
|