foreman_snapshot_management 1.0.0
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.
- 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>
|