foreman_snapshot_management 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,7 @@
1
+ module ForemanSnapshotManagement
2
+ module SnapshotHelper
3
+ def foreman_snapshot_management_snapshot_path(snapshot)
4
+ host_snapshot_path(host_id: snapshot.host, id: snapshot.id)
5
+ end
6
+ end
7
+ 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>