foreman_xen 0.7.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -1
  3. data/app/assets/javascripts/foreman_xen/xenserver/cache_refresh.js +31 -18
  4. data/app/controllers/foreman_xen/cache_controller.rb +8 -5
  5. data/app/helpers/xen_compute_helper.rb +8 -103
  6. data/app/models/concerns/fog_extensions/xenserver/host.rb +23 -0
  7. data/app/models/concerns/fog_extensions/xenserver/server.rb +21 -3
  8. data/app/models/concerns/fog_extensions/xenserver/storage_repository.rb +45 -0
  9. data/app/models/concerns/fog_extensions/xenserver/vdi.rb +11 -0
  10. data/app/models/concerns/foreman_xen/host_extensions.rb +17 -0
  11. data/app/models/foreman_xen/xenserver.rb +395 -172
  12. data/app/views/api/v1/compute_resources/xenserver.json.rabl +1 -1
  13. data/app/views/api/v2/compute_resources/xenserver.json.rabl +1 -1
  14. data/app/views/compute_resources/form/_xenserver.html.erb +2 -4
  15. data/app/views/compute_resources_vms/form/_hypervisors.html.erb +15 -11
  16. data/app/views/compute_resources_vms/form/_image_provisioning.html.erb +49 -0
  17. data/app/views/compute_resources_vms/form/_isos.html.erb +15 -0
  18. data/app/views/compute_resources_vms/form/_network_provisioning.html.erb +26 -0
  19. data/app/views/compute_resources_vms/form/_profile.html.erb +5 -0
  20. data/app/views/compute_resources_vms/form/_templates.html.erb +13 -34
  21. data/app/views/compute_resources_vms/form/xenserver/_base.html.erb +22 -178
  22. data/app/views/compute_resources_vms/form/xenserver/_network.html.erb +23 -0
  23. data/app/views/compute_resources_vms/form/xenserver/_volume.html.erb +15 -0
  24. data/app/views/compute_resources_vms/index/_xenserver.html.erb +1 -1
  25. data/app/views/images/form/_xenserver.html.erb +4 -0
  26. data/lib/foreman_xen/engine.rb +14 -3
  27. data/lib/foreman_xen/version.rb +1 -1
  28. data/test/test_helper.rb +1 -3
  29. metadata +14 -7
  30. data/app/assets/javascripts/foreman_xen/xenserver/populate_fields.js +0 -32
  31. data/app/views/compute_resources_vms/form/_network.html.erb +0 -20
  32. data/app/views/compute_resources_vms/form/_volume.html.erb +0 -18
  33. data/app/views/compute_resources_vms/form/_xenstore.html.erb +0 -119
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8538e8389348354506cb45bc49e560cd902642017bbd42a181612ba3296fe0ca
4
- data.tar.gz: a1eddbd74c4d0c5b70bbcb04efe1a5fefd68555b4229934b6a4169e86b08eda0
3
+ metadata.gz: ef04a811068d68a27dc16b361c4974e63aa95586ceba0d0d071ff167e3c17288
4
+ data.tar.gz: 9e198c6d56cc2f9f93bfa9b4eb998f1976ed7459f7559470512aa05d2fcf4eff
5
5
  SHA512:
6
- metadata.gz: a435f72d1db68dd8550a3dfa67a82f230cfdaccb71e36fa6821fa57b5f3f2d36037f4ba5dc9d42cf7a3e453eb0a94227980c792522cca2a5704e70d8e9fc6ffe
7
- data.tar.gz: c70951ebf49095228ee22742da35f14ae4dd68d8a47e908dce45d9b790de99617320d1a3abb66f214215580ded2e6daaeba5d1342a459d755479a4ea8dbe8244
6
+ metadata.gz: be7c66d98d7078da40393baa466e46904fd6bf2dc6484d9430c6f2261f103e8d5144d308fa170c06744602a85dd36f018af818997a1f5d26f13102e47776957e
7
+ data.tar.gz: 9f004290f17fb9c2d5b2eb69c7d7bf58831ce9a45beae9bbbf20543c5b1ceba95aea40d24cd8f13fc3e0e61c0b2b74569c4fad0918460c2f4216a15cde98c6e4
data/README.md CHANGED
@@ -13,6 +13,16 @@ Please see the Foreman manual for further instructions:
13
13
 
14
14
  * [Foreman: How to Install a Plugin](http://theforeman.org/plugins/#2.Installation)
15
15
 
16
+ ## Image based provisioning
17
+
18
+ In order to use the cloud-init functionality users need to:
19
+
20
+ - install the `genisoimage` package
21
+ - mount a "NFS ISO Library" (as XenServer calls it) which is attached to the Xen pool to a location writable by the foreman user.
22
+ - set this mount point / path as ISO library mountpoint in the compute resource
23
+
24
+ foreman_xen then creates a network configuration file, renders the user_data template, puts them in an ISO, copies this ISO to the attached ISO-library and attaches it to the created VM, where cloud-init can use the data provided to initialize the VM.
25
+
16
26
  ## Compatibility
17
27
 
18
28
  | Foreman Version | Plugin Version |
@@ -24,7 +34,8 @@ Please see the Foreman manual for further instructions:
24
34
  | >=1.13, <1.14 | 0.4.x (unmaintained) |
25
35
  | >=1.14, <1.17 | 0.5.x (unmaintained) |
26
36
  | >=1.17, <1.18 | 0.6.x (unmaintained) |
27
- | >=1.18 | 0.7.x |
37
+ | >=1.18, <1.20 | 0.7.x (unmaintained) |
38
+ | >=1.20 | 1.0.0 |
28
39
 
29
40
  ## Support
30
41
 
@@ -1,20 +1,33 @@
1
- function refreshCache(item, on_success) {
2
- tfm.tools.showSpinner();
3
- attribute_name = $(item).data('attribute')
4
- data = {
5
- type: attribute_name,
6
- compute_resource_id: $(item).data('compute-resource-id')
1
+ function refreshCache(item) {
2
+ tfm.tools.showSpinner();
3
+ attribute_name = $(item).data('attribute');
4
+ sel = $(item).closest('.input-group').children('select')
5
+ data = {
6
+ type: attribute_name,
7
+ compute_resource_id: $(item).data('compute-resource-id')
8
+ };
9
+ $.ajax({
10
+ type:'post',
11
+ url: $(item).data('url'),
12
+ data: data,
13
+ complete: function(){
14
+ tfm.tools.hideSpinner();
15
+ },
16
+ error: function(){
17
+ notify(__("Error refreshing cache for " + attribute_name), 'error', true);
18
+ },
19
+ success: function(results, textStatus, jqXHR){
20
+ var elements = sel.children()
21
+ if (elements.first().val() == "") { //include_empty
22
+ elements = elements.slice(1);
23
+ }
24
+ elements.remove();
25
+ for (var i = 0; i < results.length; i++) {
26
+ var result = results[i];
27
+ var id = ('uuid' in result) ? result['uuid'] : result['id'];
28
+ var name = ('display_name' in result) ? result['display_name'] : result['name'];
29
+ sel.append('<option value=' + id + '>' + name + '</option>');
30
+ }
7
31
  }
8
- $.ajax({
9
- type:'post',
10
- url: $(item).data('url'),
11
- data: data,
12
- complete: function(){
13
- tfm.tools.hideSpinner();
14
- },
15
- error: function(){
16
- notify(__("Error refreshing cache for " + attribute_name), 'error', true);
17
- },
18
- success: on_success
19
- })
32
+ });
20
33
  }
@@ -6,9 +6,7 @@ module ForemanXen
6
6
  def refresh
7
7
  type = params[:type]
8
8
 
9
- unless cache_attribute_whitelist.include?(type)
10
- process_error(:error_msg => "Error refreshing cache. #{type} is not a white listed attribute")
11
- end
9
+ process_error(:error_msg => "Error refreshing cache. #{type} is not a white listed attribute") unless cache_attribute_whitelist.include?(type)
12
10
 
13
11
  unless @compute_resource.respond_to?("#{type}!")
14
12
  process_error(:error_msg => "Error refreshing cache. Method '#{type}!' not found for compute resource" +
@@ -16,7 +14,12 @@ module ForemanXen
16
14
  end
17
15
 
18
16
  respond_to do |format|
19
- format.json { render :json => @compute_resource.public_send("#{type}!") }
17
+ format.json do
18
+ filtered_data = @compute_resource.public_send("#{type}!").map do |e|
19
+ e.attributes.slice(:id, :uuid, :name, :display_name)
20
+ end
21
+ render json: filtered_data
22
+ end
20
23
  end
21
24
  end
22
25
 
@@ -24,7 +27,7 @@ module ForemanXen
24
27
 
25
28
  # List of methods to permit
26
29
  def cache_attribute_whitelist
27
- %w[networks hypervisors templates custom_templates builtin_templates storage_pools]
30
+ %w[isos networks available_hypervisors hypervisors templates builtin_templates storage_pools]
28
31
  end
29
32
 
30
33
  def load_compute_resource
@@ -1,121 +1,26 @@
1
1
  module XenComputeHelper
2
- def compute_attribute_map(params, compute_resource, new)
3
- if controller_name == 'hosts'
4
- attribute_map = hosts_controller_compute_attribute_map(params, compute_resource, new)
5
- elsif controller_name == 'compute_attributes'
6
- attribute_map = compute_resource_controller_attribute_map(params, compute_resource)
7
- end
8
- attribute_map
9
- end
2
+ def compute_attributes_from_params(compute_resource)
3
+ id = params.dig('host', 'compute_profile_id') || params.dig('compute_profile_id')
4
+ return compute_resource.compute_profile_attributes_for id if id
10
5
 
11
- def init_vmdata
12
- vmdata = {
13
- :ifs => {
14
- '0' => {
15
- :ip => '',
16
- :gateway => '',
17
- :netmask => ''
18
- }
19
- },
20
- :nameserver1 => '',
21
- :nameserver2 => '',
22
- :environment => ''
23
- }
6
+ {}
24
7
  end
25
8
 
26
9
  private
27
10
 
28
- def hosts_controller_compute_attribute_map(params, compute_resource, new)
29
- attribute_map = empty_attribute_map
30
- if new.try(:new_record?)
31
- compute_attributes = compute_resource.compute_profile_attributes_for(params['host']['compute_profile_id'])
32
- attribute_map = filter_compute_attributes(attribute_map, compute_attributes)
33
- elsif new
34
- attribute_map[:cpu_count] = new.vcpus_max || nil
35
- attribute_map[:memory_min] = new.memory_static_min || nil
36
- attribute_map[:memory_max] = new.memory_static_max || nil
37
- if new.vbds
38
- vdi = new.vbds.first.vdi
39
- if vdi
40
- attribute_map[:volume_selected] = vdi.sr.uuid || nil
41
- attribute_map[:volume_size] = vdi.virtual_size ? (vdi.virtual_size.to_i / 1_073_741_824).to_s : nil
42
- end
43
- end
44
- attribute_map[:network_selected] = new.vifs.first.network.name || nil if new.vifs
45
- end
46
- attribute_map
47
- end
48
-
49
- def compute_resource_controller_attribute_map(params, compute_resource)
50
- attribute_map = empty_attribute_map
51
- if params && params['compute_profile_id']
52
- compute_attributes = compute_resource.compute_profile_attributes_for(params['compute_profile_id'])
53
- elsif params && params['host'] && params['host']['compute_profile_id']
54
- compute_attributes = compute_resource.compute_profile_attributes_for(params['host']['compute_profile_id'])
55
- end
56
- attribute_map = filter_compute_attributes(attribute_map, compute_attributes) if compute_attributes
57
- attribute_map
58
- end
59
-
60
- def empty_attribute_map
61
- { :volume_size => nil,
62
- :volume_selected => nil,
63
- :network_selected => nil,
64
- :template_selected_custom => nil,
65
- :template_selected_builtin => nil,
66
- :cpu_count => nil,
67
- :memory_min => nil,
68
- :memory_max => nil,
69
- :power_on => nil }
70
- end
71
-
72
- def filter_compute_attributes(attribute_map, compute_attributes)
73
- if compute_attributes['VBDs']
74
- attribute_map[:volume_size] = compute_attributes['VBDs']['physical_size']
75
- attribute_map[:volume_selected] = compute_attributes['VBDs']['sr_uuid']
76
- end
77
- attribute_map[:network_selected] = compute_attributes['VIFs']['print'] if compute_attributes['VIFs']
78
- attribute_map[:template_selected_custom] = compute_attributes['custom_template_name']
79
- attribute_map[:template_selected_builtin] = compute_attributes['builtin_template_name']
80
- attribute_map[:cpu_count] = compute_attributes['vcpus_max']
81
- attribute_map[:memory_min] = compute_attributes['memory_min']
82
- attribute_map[:memory_max] = compute_attributes['memory_max']
83
- attribute_map[:power_on] = compute_attributes['start']
84
- attribute_map
85
- end
86
-
87
- def xen_builtin_template_map(compute_resource)
88
- compute_resource.builtin_templates.map { |t| [t.name, t.name] }
89
- end
90
-
91
- def xen_custom_template_map(compute_resource)
92
- compute_resource.custom_templates.map { |t| [t.name, t.name] }
93
- end
94
-
95
- def xen_storage_pool_map(compute_resource)
96
- compute_resource.storage_pools.map { |item| [item[:display_name], item[:uuid]] }
97
- end
98
-
99
- def xen_hypervisor_map(compute_resource)
100
- compute_resource.available_hypervisors!.map do |t|
101
- [t.name + ' - ' + (
102
- t.metrics.memory_free.to_f / t.metrics.memory_total.to_f * 100
103
- ).round(2).to_s + '% free mem', t.name]
104
- end
105
- end
106
-
107
11
  def selectable_f_with_cache_invalidation(f, attr, array,
108
12
  select_options = {}, html_options = {}, input_group_options = {})
109
13
  unless html_options.key?('input_group_btn')
110
14
  html_options[:input_group_btn] = link_to_function(
111
15
  icon_text('refresh'),
112
- "refreshCache(this, #{input_group_options[:callback]})",
16
+ 'refreshCache(this)',
113
17
  :class => 'btn btn-primary',
114
18
  :title => _(input_group_options[:title]),
115
19
  :data => {
116
20
  :url => input_group_options[:url],
117
- :compute_resource_id => input_group_options[:computer_resource_id],
118
- :attribute => input_group_options[:attribute]
21
+ :compute_resource_id => input_group_options[:compute_resource_id],
22
+ :attribute => input_group_options[:attribute],
23
+ :select_attr => attr
119
24
  }
120
25
  )
121
26
  end
@@ -0,0 +1,23 @@
1
+ module FogExtensions
2
+ module Xenserver
3
+ module Host
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attribute :display_name
8
+ prepend FogExtensions::Xenserver::Host
9
+ end
10
+
11
+ def initialize(new_attributes = {})
12
+ super(new_attributes)
13
+ attributes[:display_name] = "#{name} - #{mem_free_gb} GB free memory"
14
+ end
15
+
16
+ def mem_free_gb
17
+ return metrics.memory_free.to_i / 1024 / 1024 / 1024 if metrics
18
+
19
+ 0
20
+ end
21
+ end
22
+ end
23
+ end
@@ -5,8 +5,13 @@ module FogExtensions
5
5
 
6
6
  include ActionView::Helpers::NumberHelper
7
7
 
8
- attr_accessor :start
9
- attr_accessor :memory_min, :memory_max, :custom_template_name, :builtin_template_name, :hypervisor_host
8
+ attr_accessor :start, :image_id, :hypervisor_host, :iso, :target_sr
9
+ attr_accessor :memory_min, :memory_max, :builtin_template
10
+ attr_writer :volumes, :interfaces
11
+
12
+ def id
13
+ uuid
14
+ end
10
15
 
11
16
  def to_s
12
17
  name
@@ -16,6 +21,13 @@ module FogExtensions
16
21
 
17
22
  def volumes_attributes=(attrs); end
18
23
 
24
+ def volumes
25
+ @volumes ||= []
26
+ disks = vbds.compact.select(&:disk?)
27
+ disks.sort! { |x, y| x.userdevice <=> y.userdevice }
28
+ (disks.map(&:vdi) + @volumes).uniq
29
+ end
30
+
19
31
  def memory
20
32
  memory_static_max.to_i
21
33
  end
@@ -45,12 +57,18 @@ module FogExtensions
45
57
  end
46
58
 
47
59
  def interfaces
48
- vifs
60
+ (vifs + @interfaces).uniq
49
61
  end
50
62
 
51
63
  def select_nic(fog_nics, nic)
52
64
  fog_nics[0]
53
65
  end
66
+
67
+ def user_data
68
+ return !other_config['default_template'] if is_a_template
69
+
70
+ false
71
+ end
54
72
  end
55
73
  end
56
74
  end
@@ -0,0 +1,45 @@
1
+ module FogExtensions
2
+ module Xenserver
3
+ module StorageRepository
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attribute :display_name
8
+ prepend FogExtensions::Xenserver::StorageRepository
9
+ end
10
+
11
+ def initialize(new_attributes = {})
12
+ super(new_attributes)
13
+ attributes[:display_name] = init_display_name
14
+ end
15
+
16
+ def free_space
17
+ physical_size.to_i - physical_utilisation.to_i
18
+ end
19
+
20
+ def free_space_gb
21
+ free_space.to_i / 1024 / 1024 / 1024
22
+ end
23
+
24
+ def physical_size_gb
25
+ physical_size.to_i / 1024 / 1024 / 1024
26
+ end
27
+
28
+ def physical_utilisation_gb
29
+ physical_utilisation.to_i / 1024 / 1024 / 1024
30
+ end
31
+
32
+ def init_display_name
33
+ srname = name
34
+ unless shared
35
+ pbd = pbds.first
36
+ srname = "#{name} - #{pbd.host.name}" unless pbd.nil?
37
+ end
38
+ format('%{n} (%{f}: %{f_gb} GB - %{u}: %{u_gb} GB - %{t}: %{t_gb} GB)',
39
+ n: srname, f: _('free'), f_gb: free_space_gb,
40
+ u: _('used'), u_gb: physical_utilisation_gb,
41
+ t: _('total'), t_gb: physical_size_gb)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,11 @@
1
+ module FogExtensions
2
+ module Xenserver
3
+ module Vdi
4
+ extend ActiveSupport::Concern
5
+
6
+ def id
7
+ uuid
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module ForemanXen
2
+ module HostExtensions
3
+ extend ActiveSupport::Concern
4
+
5
+ def built(installed = true)
6
+ compute_resource.cleanup_configdrive(uuid) if compute_resource && compute_resource.type == 'ForemanXen::Xenserver'
7
+ super(installed)
8
+ end
9
+
10
+ def disassociate!
11
+ # Disassociated host object cannot be saved unless provision_method
12
+ # is supported by the default compute resource
13
+ self.provision_method = 'build'
14
+ super
15
+ end
16
+ end
17
+ end
@@ -2,19 +2,63 @@ module ForemanXen
2
2
  class Xenserver < ComputeResource
3
3
  validates :url, :user, :password, :presence => true
4
4
 
5
+ GB_BYTES = 1_073_741_824 # 1gb in bytes
6
+
5
7
  def provided_attributes
6
8
  super.merge(
7
- :uuid => :reference,
9
+ :uuid => :uuid,
8
10
  :mac => :mac
9
11
  )
10
12
  end
11
13
 
12
14
  def capabilities
13
- [:build]
15
+ %i[build image new_volume]
16
+ end
17
+
18
+ def host_compute_attrs(host)
19
+ super(host).merge(
20
+ name_description: host.comment,
21
+ is_a_template: false,
22
+ is_a_shapshot: false,
23
+ xenstore: host_xenstore_data(host),
24
+ network_data: host_network_data(host)
25
+ )
26
+ end
27
+
28
+ def user_data_supported
29
+ true
30
+ end
31
+
32
+ def iso_library_mountpoint
33
+ attrs[:iso_library_mountpoint]
34
+ end
35
+
36
+ def iso_library_mountpoint=(path)
37
+ mountpoint = path.to_s.end_with?('/') ? path.to_s : "#{path}/"
38
+ mountpoint = nil if path.to_s.strip.empty?
39
+ attrs[:iso_library_mountpoint] = mountpoint
40
+ end
41
+
42
+ def cleanup_configdrive(uuid)
43
+ iso_file_name = "foreman-configdrive-#{uuid}.iso"
44
+ begin
45
+ path = File.join(iso_library_mountpoint, iso_file_name)
46
+ exist = File.exist? path
47
+ FileUtils.rm(path) if exist
48
+ rescue
49
+ return true unless exist
50
+
51
+ return false
52
+ end
14
53
  end
15
54
 
55
+ # rubocop:disable Rails/DynamicFindBy
56
+ # Fog::XenServer::Compute (client) isn't an ActiveRecord model which
57
+ # supports find_by()
16
58
  def find_vm_by_uuid(uuid)
17
- client.servers.get(uuid)
59
+ return client.servers.find { |s| s.reference == uuid } if uuid.start_with? 'OpaqueRef:'
60
+
61
+ client.servers.find_by_uuid(uuid)
18
62
  rescue Fog::XenServer::RequestFailed => e
19
63
  Foreman::Logging.exception("Failed retrieving xenserver vm by uuid #{uuid}", e)
20
64
  raise(ActiveRecord::RecordNotFound) if e.message.include?('HANDLE_INVALID')
@@ -22,10 +66,12 @@ module ForemanXen
22
66
 
23
67
  raise e
24
68
  end
69
+ # rubocop:enable Rails/DynamicFindBy
25
70
 
26
71
  # we default to destroy the VM's storage as well.
27
72
  def destroy_vm(ref, args = {})
28
73
  logger.info "destroy_vm: #{ref} #{args}"
74
+ cleanup_configdrive(ref) if iso_library_mountpoint
29
75
  find_vm_by_uuid(ref).destroy
30
76
  rescue ActiveRecord::RecordNotFound
31
77
  true
@@ -60,23 +106,39 @@ module ForemanXen
60
106
  errors[:base] << e.message
61
107
  end
62
108
 
109
+ def available_images
110
+ custom_templates!
111
+ end
112
+
63
113
  def available_hypervisors
64
- read_from_cache('available_hypervisors', 'available_hypervisors!')
114
+ hypervisors.select(&:enabled)
65
115
  end
66
116
 
67
117
  def available_hypervisors!
68
- store_in_cache('available_hypervisors') do
118
+ hypervisors!.select(&:enabled)
119
+ end
120
+
121
+ def hypervisors
122
+ read_from_cache('hypervisors', 'hypervisors!')
123
+ end
124
+
125
+ def hypervisors!
126
+ store_in_cache('hypervisors') do
69
127
  hosts = client.hosts
70
128
  hosts.sort_by(&:name)
71
129
  end
72
130
  end
73
131
 
74
132
  def new_nic(attr = {})
75
- client.networks.new attr
133
+ client.vifs.new attr
76
134
  end
77
135
 
78
136
  def new_volume(attr = {})
79
- client.storage_repositories.new attr
137
+ size = attr[:virtual_size_gb].to_i * GB_BYTES
138
+ vdi = client.vdis.new virtual_size: size.to_s
139
+ vdi.type = 'user'
140
+ vdi.sr = storage_pools.find { |s| s.uuid == attr[:sr].to_s } if attr[:sr]
141
+ vdi
80
142
  end
81
143
 
82
144
  def storage_pools
@@ -85,34 +147,42 @@ module ForemanXen
85
147
 
86
148
  def storage_pools!
87
149
  store_in_cache('storage_pools') do
88
- results = []
89
- storages = client.storage_repositories.select { |sr| sr.type != 'udev' && sr.type != 'iso' }
90
- storages.each do |sr|
91
- subresults = {}
92
- found = false
93
-
94
- available_hypervisors.each do |host|
95
- next unless sr.reference == host.suspend_image_sr
96
-
97
- found = true
98
- subresults[:name] = sr.name
99
- subresults[:display_name] = sr.name + '(' + host.hostname + ')'
100
- subresults[:uuid] = sr.uuid
101
- break
102
- end
103
- unless found
104
- subresults[:name] = sr.name
105
- subresults[:display_name] = sr.name
106
- subresults[:uuid] = sr.uuid
107
- end
108
- results.push(subresults)
150
+ pools = client.storage_repositories.select do |sr|
151
+ sr.type != 'udev' && sr.type != 'iso'
109
152
  end
110
- results.sort_by! { |item| item[:display_name] }
153
+ pools.sort_by(&:display_name)
154
+ end
155
+ end
156
+
157
+ def isos
158
+ all_isos.reject do |iso|
159
+ iso.name =~ /foreman-configdrive/
160
+ end
161
+ end
162
+
163
+ def isos!
164
+ all_isos!.reject do |iso|
165
+ iso.name =~ /foreman-configdrive/
166
+ end
167
+ end
168
+
169
+ def all_isos
170
+ read_from_cache('isos', 'isos!')
171
+ end
172
+
173
+ def all_isos!
174
+ store_in_cache('isos') do
175
+ isos = iso_libraries.map(&:vdis).flatten
176
+ isos.sort_by(&:name)
111
177
  end
112
178
  end
113
179
 
180
+ def new_interface(attr = {})
181
+ client.vifs.new attr
182
+ end
183
+
114
184
  def interfaces
115
- client.interfaces
185
+ client.vifs
116
186
  rescue
117
187
  []
118
188
  end
@@ -186,158 +256,137 @@ module ForemanXen
186
256
  end
187
257
 
188
258
  def new_vm(attr = {})
189
- test_connection
190
- return unless errors.empty?
191
-
192
- opts = vm_instance_defaults.merge(attr.to_hash).symbolize_keys
193
-
194
- %i[networks volumes].each do |collection|
195
- nested_attrs = opts.delete("#{collection}_attributes".to_sym)
196
- opts[collection] = nested_attributes_for(collection, nested_attrs) if nested_attrs
259
+ attr = attr.to_hash.deep_symbolize_keys
260
+ %i[networks interfaces].each do |collection|
261
+ nested_attr = attr.delete("#{collection}_attributes".to_sym)
262
+ attr[collection] = nested_attributes_for(collection, nested_attr) if nested_attr
263
+ end
264
+ if attr[:volumes_attributes]
265
+ vol_attr = nested_attributes_for('volumes', attr[:volumes_attributes])
266
+ attr[:volumes] = vol_attr.map { |v| new_volume(v) }
197
267
  end
198
- opts.reject! { |_, v| v.nil? }
199
- client.servers.new opts
268
+ attr.reject! { |_, v| v.nil? }
269
+ super(attr)
270
+ end
271
+
272
+ def vm_attr_from_args(args)
273
+ {
274
+ name: args[:name],
275
+ name_description: args[:comment],
276
+ VCPUs_max: args[:vcpus_max],
277
+ VCPUs_at_startup: args[:vcpus_max],
278
+ memory_static_max: args[:memory_max],
279
+ memory_dynamic_max: args[:memory_max],
280
+ memory_dynamic_min: args[:memory_min],
281
+ memory_static_min: args[:memory_min]
282
+ }
200
283
  end
201
284
 
202
285
  def create_vm(args = {})
203
- custom_template_name = args[:custom_template_name].to_s
204
- builtin_template_name = args[:builtin_template_name].to_s
205
-
206
- if builtin_template_name != '' && custom_template_name != ''
207
- logger.info "custom_template_name: #{custom_template_name}"
208
- logger.info "builtin_template_name: #{builtin_template_name}"
209
- raise 'you can select at most one template type'
210
- end
286
+ args = args.deep_symbolize_keys
287
+ logger.debug('create_vm args:')
288
+ logger.debug(args)
211
289
  begin
212
- logger.info "create_vm(): custom_template_name: #{custom_template_name}"
213
- logger.info "create_vm(): builtin_template_name: #{builtin_template_name}"
214
- vm = custom_template_name != '' ? create_vm_from_custom(args) : create_vm_from_builtin(args)
215
- vm.set_attribute('name_description', 'Provisioned by Foreman')
216
- vm.set_attribute('VCPUs_max', args[:vcpus_max])
217
- vm.set_attribute('VCPUs_at_startup', args[:vcpus_max])
218
- vm.reload
219
- return vm
220
- rescue => e
221
- logger.info e
222
- logger.info e.backtrace.join("\n")
223
- return false
224
- end
225
- end
290
+ # Create VM Object
291
+ attr = vm_attr_from_args(args)
292
+ if args[:provision_method] == 'image'
293
+ image = available_images.find { |i| i.uuid == args[:image_id].to_s }
294
+ sr = storage_pools.find { |s| s.uuid == args[:target_sr].to_s }
295
+ vm = create_vm_from_image(image, attr, sr)
296
+ else
297
+ template = builtin_templates.find { |t| t.uuid == args[:builtin_template].to_s }
298
+ raise 'Template not found' unless template
226
299
 
227
- def create_vm_from_custom(args)
228
- mem_max = args[:memory_max]
229
- mem_min = args[:memory_min]
300
+ vm = create_vm_from_template(attr, template)
301
+ end
230
302
 
231
- host = get_hypervisor_host(args)
303
+ raise 'Error creating VM' unless vm
232
304
 
233
- logger.info "create_vm_from_builtin: host : #{host.name}"
305
+ # Set correct affinity
306
+ set_vm_affinity(vm, args[:hypervisor_host].to_s)
234
307
 
235
- raise 'Memory max cannot be lower than Memory min' if mem_min.to_i > mem_max.to_i
308
+ # Add NICs
309
+ vm.interfaces = args[:interfaces_attributes].map do |_, v|
310
+ create_interface(vm, v[:network])
311
+ end
236
312
 
237
- template = client.custom_templates.select { |t| t.name == args[:custom_template_name] }.first
238
- vm = template.clone args[:name]
239
- vm.affinity = host
313
+ # Attach ConfigDrive
314
+ create_and_attach_configdrive(vm, args) if args[:configdrive] == '1' && args[:provision_method] == 'image'
240
315
 
241
- vm.provision
316
+ # Attach ISO
317
+ unless args[:iso].empty?
318
+ iso_vdi = isos.find { |i| i.uuid == args[:iso] }
319
+ attach_iso(vm, iso_vdi)
320
+ end
242
321
 
243
- begin
244
- vm.vifs.first.destroy
245
- rescue
246
- nil
247
- end
322
+ # Add new Volumes
323
+ unless args[:volumes_attributes].nil?
324
+ vm.volumes = args[:volumes_attributes].map do |_, v|
325
+ create_volume(vm, v) unless v[:_delete] == '1'
326
+ end
327
+ end
248
328
 
249
- create_network(vm, args)
329
+ # Write XenStore data
330
+ xenstore_data = xenstore_set_mac(vm, args[:xenstore])
331
+ set_xenstore_data(vm, xenstore_data)
250
332
 
251
- args['xenstore']['vm-data']['ifs']['0']['mac'] = vm.vifs.first.mac
252
- xenstore_data = xenstore_hash_flatten(args['xenstore'])
333
+ # Fix Description
334
+ vm.set_attribute('name-description', args[:name_description])
253
335
 
254
- vm.set_attribute('xenstore_data', xenstore_data)
255
- if vm.memory_static_max.to_i < mem_max.to_i
256
- vm.set_attribute('memory_static_max', mem_max)
257
- vm.set_attribute('memory_dynamic_max', mem_max)
258
- vm.set_attribute('memory_dynamic_min', mem_min)
259
- vm.set_attribute('memory_static_min', mem_min)
260
- else
261
- vm.set_attribute('memory_static_min', mem_min)
262
- vm.set_attribute('memory_dynamic_min', mem_min)
263
- vm.set_attribute('memory_dynamic_max', mem_max)
264
- vm.set_attribute('memory_static_max', mem_max)
336
+ return vm
337
+ rescue => e
338
+ cleanup_configdrive(vm.uuid) if vm&.uuid
339
+ vm&.destroy
340
+ vm.volumes.each(&:destroy) if vm&.volumes
341
+ logger.info e
342
+ logger.info e.backtrace.join("\n")
343
+ raise e
265
344
  end
345
+ end
266
346
 
267
- disks = vm.vbds.select { |vbd| vbd.type == 'Disk' }
268
- disks.sort! { |a, b| a.userdevice <=> b.userdevice }
269
- i = 0
270
- disks.each do |vbd|
271
- vbd.vdi.set_attribute('name-label', "#{args[:name]}_#{i}")
272
- i += 1
347
+ def create_vm_from_template(attr, template)
348
+ vm_attr = template.attributes.dup.merge(attr)
349
+ %i[uuid domid reference allowed_operations].each do |a|
350
+ vm_attr.delete(a)
273
351
  end
352
+ vm_attr[:is_a_template] = false
353
+ vm_attr[:other_config].delete('default_template')
354
+ vm_attr[:other_config]['mac_seed'] = SecureRandom.uuid
355
+ vm = new_vm(vm_attr)
356
+ # Set any host affinity (required for saving) - correct later
357
+ vm.affinity = client.hosts.first
358
+ vm.save
274
359
  vm
275
360
  end
276
361
 
277
- def create_vm_from_builtin(args)
278
- mem_max = args[:memory_max]
279
- mem_min = args[:memory_min]
280
-
281
- host = get_hypervisor_host(args)
282
-
283
- logger.info "create_vm_from_builtin: host : #{host.name}"
284
-
285
- builtin_template_name = args[:builtin_template_name]
286
- builtin_template_name = builtin_template_name.to_s
287
-
288
- storage_repository = client.storage_repositories.find { |sr| sr.uuid == (args[:VBDs][:sr_uuid]).to_s }
289
-
290
- gb = 1_073_741_824 # 1gb in bytes
291
- size = args[:VBDs][:physical_size].to_i * gb
292
- vdi = client.vdis.create :name => "#{args[:name]}-disk1",
293
- :storage_repository => storage_repository,
294
- :description => "#{args[:name]}-disk_1",
295
- :virtual_size => size.to_s
362
+ def create_vm_from_image(image, attr, sr)
363
+ vm_ref = client.copy_server image.reference, attr[:name], sr.reference
364
+ client.provision_server vm_ref
365
+ vm = client.servers.find { |s| s.reference == vm_ref }
366
+ set_vm_profile_attributes(vm, attr)
367
+ rename_cloned_volumes(vm)
368
+ vm
369
+ end
296
370
 
297
- other_config = {}
298
- if builtin_template_name != ''
299
- template = client.builtin_templates.find { |tmp| tmp.name == args[:builtin_template_name] }
300
- other_config = template.other_config
301
- other_config.delete 'disks'
302
- other_config.delete 'default_template'
303
- other_config['mac_seed'] = SecureRandom.uuid
371
+ def set_vm_profile_attributes(vm, attr)
372
+ # Memory limits must satisfy:
373
+ # static_min <= dynamic_min <= dynamic_max <= static_max
374
+ mem = %w[memory_static_max memory_dynamic_max
375
+ memory_dynamic_min memory_static_min]
376
+ mem.reverse! if vm.memory_static_max.to_i > attr[:memory_static_max].to_i
377
+ # VCPU values must satisfy: 0 < VCPUs_at_startup <= VCPUs_max
378
+ cpu = %w[VCPUs_max VCPUs_at_startup]
379
+ cpu.reverse! if vm.vcpus_at_startup > attr[:VCPUs_at_startup]
380
+ (mem + cpu).each { |e| vm.set_attribute e, attr[e.to_sym] }
381
+ end
382
+
383
+ def rename_cloned_volumes(vm)
384
+ vm.volumes.each do |vol|
385
+ udev = vol.vbds.find { |v| v.vm.uuid == vm.uuid }.userdevice
386
+ name = "#{vm.name}-#{udev}"
387
+ vol.set_attribute 'name-label', name
388
+ vol.set_attribute 'name-description', name
304
389
  end
305
- vm = client.servers.new :name => args[:name],
306
- :affinity => host,
307
- :pv_bootloader => '',
308
- :hvm_boot_params => { :order => 'dnc' },
309
- :other_config => other_config,
310
- :memory_static_max => mem_max,
311
- :memory_static_min => mem_min,
312
- :memory_dynamic_max => mem_max,
313
- :memory_dynamic_min => mem_min
314
-
315
- vm.save :auto_start => false
316
- client.vbds.create :vm => vm, :vdi => vdi
317
-
318
- create_network(vm, args)
319
-
320
- if args[:xstools] == '1'
321
- # Add xs-tools ISO to newly created VMs
322
- dvd_vdi = client.vdis.find { |isovdi| isovdi.name == 'xs-tools.iso' || isovdi.name == 'guest-tools.iso' }
323
- vbdconnectcd = {
324
- 'vdi' => dvd_vdi,
325
- 'vm' => vm.reference,
326
- 'userdevice' => '1',
327
- 'mode' => 'RO',
328
- 'type' => 'cd',
329
- 'other_config' => {},
330
- 'qos_algorithm_type' => '',
331
- 'qos_algorithm_params' => {}
332
- }
333
- vm.vbds = client.vbds.create vbdconnectcd
334
- vm.reload
335
- end
336
-
337
- vm.provision
338
- vm.set_attribute('HVM_boot_policy', 'BIOS order')
339
- vm.reload
340
- vm
341
390
  end
342
391
 
343
392
  def console(uuid)
@@ -373,9 +422,10 @@ module ForemanXen
373
422
 
374
423
  def client
375
424
  @client ||= Fog::XenServer::Compute.new(
376
- :xenserver_url => url,
377
- :xenserver_username => user,
378
- :xenserver_password => password
425
+ xenserver_url: url,
426
+ xenserver_username: user,
427
+ xenserver_password: password,
428
+ xenserver_timeout: 1800
379
429
  )
380
430
  end
381
431
 
@@ -384,30 +434,94 @@ module ForemanXen
384
434
  @client = nil
385
435
  end
386
436
 
387
- def vm_instance_defaults
388
- super.merge({})
389
- end
390
-
391
437
  private
392
438
 
393
- def create_network(vm, args)
394
- net = client.networks.find { |n| n.name == args[:VIFs][:print] }
439
+ def create_volume(vm, attr)
440
+ vdi = new_volume attr
441
+ udev = find_free_userdevice(vm)
442
+ vdi.name = "#{vm.name}-#{udev}"
443
+ vdi.description = "#{vm.name}-#{udev}"
444
+ vdi.save
445
+ # Attach VDI to VM
446
+ client.vbds.create vm: vm, vdi: vdi, userdevice: udev.to_s, bootable: true
447
+ vdi
448
+ end
449
+
450
+ def create_interface(vm, network_uuid)
451
+ net = client.networks.find { |n| n.uuid == network_uuid }
452
+ devices = vm.vifs.map(&:device)
453
+ device = 0
454
+ device += 1 while devices.include?(device.to_s)
395
455
  net_config = {
396
456
  'mac_autogenerated' => 'True',
397
457
  'vm' => vm.reference,
398
458
  'network' => net.reference,
399
459
  'mac' => '',
400
- 'device' => '0',
460
+ 'device' => device.to_s,
401
461
  'mtu' => '0',
402
462
  'other_config' => {},
403
463
  'qos_algorithm_type' => 'ratelimit',
404
464
  'qos_algorithm_params' => {}
405
465
  }
406
- vm.vifs = client.vifs.create net_config
407
- vm.reload
466
+ client.vifs.create net_config
467
+ end
468
+
469
+ def attach_iso(vm, iso_vdi)
470
+ cd_drive = client.vbds.find { |v| v.vm == vm && v.type == 'CD' }
471
+ if cd_drive&.empty
472
+ client.insert_vbd cd_drive.reference, iso_vdi.reference
473
+ else
474
+ # Windows VMs expect the CDROM drive on userdevice 3
475
+ vbds = client.vbds.select { |v| v.vm == vm }
476
+ udev = vbds.map(&:userdevice).include?('3') ? find_free_userdevice(vm) : '3'
477
+ vbd = {
478
+ 'vdi' => iso_vdi,
479
+ 'vm' => vm,
480
+ 'userdevice' => udev.to_s,
481
+ 'mode' => 'RO',
482
+ 'type' => 'CD',
483
+ 'other_config' => {},
484
+ 'qos_algorithm_type' => '',
485
+ 'qos_algorithm_params' => {}
486
+ }
487
+ client.vbds.create vbd
488
+ end
489
+ true
490
+ end
491
+
492
+ def find_free_userdevice(vm)
493
+ # Find next free userdevice id for vbd
494
+ # vm.vbds is not current, vm.reload not working.
495
+ vbds = client.vbds.select { |v| v.vm == vm }
496
+ userdevices = vbds.map(&:userdevice)
497
+ udev = 0
498
+ udev += 1 while userdevices.include?(udev.to_s)
499
+ udev
500
+ end
501
+
502
+ def xenstore_set_mac(vm, xenstore_data)
503
+ xenstore_data[:'vm-data'][:ifs][:'0'][:mac] = vm.interfaces.first.mac
504
+ xenstore_data
505
+ end
506
+
507
+ def set_xenstore_data(vm, xenstore_data)
508
+ xenstore_data = xenstore_hash_flatten(xenstore_data)
509
+ vm.set_attribute('xenstore_data', xenstore_data)
408
510
  end
409
511
 
410
- def xenstore_hash_flatten(nested_hash, key = nil, keychain = nil, out_hash = {})
512
+ def host_xenstore_data(host)
513
+ p_if = host.primary_interface
514
+ subnet = p_if.subnet || p_if.subnet6
515
+ { 'vm-data' => { 'ifs' => { '0' =>
516
+ { 'ip' => p_if.ip.empty? ? p_if.ip6 : p_if.ip,
517
+ 'gateway' => subnet.nil? ? '' : subnet.gateway,
518
+ 'netmask' => subnet.nil? ? '' : subnet.mask } } },
519
+ 'nameserver1' => subnet.nil? ? '' : subnet.dns_primary,
520
+ 'nameserver2' => subnet.nil? ? '' : subnet.dns_secondary,
521
+ 'environment' => host.environment.to_s }
522
+ end
523
+
524
+ def xenstore_hash_flatten(nested_hash, _key = nil, keychain = nil, out_hash = {})
411
525
  nested_hash.each do |k, v|
412
526
  if v.is_a? Hash
413
527
  xenstore_hash_flatten(v, k, "#{keychain}#{k}/", out_hash)
@@ -418,6 +532,115 @@ module ForemanXen
418
532
  out_hash
419
533
  end
420
534
 
535
+ # rubocop:disable Rails/DynamicFindBy
536
+ # Fog::XenServer::Compute (client) isn't an ActiveRecord model which
537
+ # supports find_by()
538
+ def set_vm_affinity(vm, hypervisor)
539
+ if hypervisor.empty?
540
+ vm.set_attribute('affinity', '')
541
+ else
542
+ vm.set_attribute('affinity', client.hosts.find_by_uuid(hypervisor))
543
+ end
544
+ end
545
+ # rubocop:enable Rails/DynamicFindBy
546
+
547
+ def create_and_attach_configdrive(vm, attr)
548
+ network_data = add_mac_to_network_data(attr[:network_data], vm)
549
+ iso_name = generate_configdrive(vm.uuid,
550
+ vm_meta_data(vm).to_json,
551
+ network_data.deep_stringify_keys.to_json,
552
+ attr[:user_data],
553
+ iso_library_mountpoint)
554
+ rescan_iso_libraries
555
+ iso_vdi = all_isos!.find { |iso| iso.name == iso_name }
556
+ raise 'Unable to locate metadata iso on iso libraries' unless iso_vdi
557
+
558
+ attach_iso(vm, iso_vdi)
559
+ end
560
+
561
+ def vm_meta_data(vm)
562
+ { 'uuid' => vm.uuid, 'hostname' => vm.name }
563
+ end
564
+
565
+ # openstack configdive network_data format spec:
566
+ # https://github.com/openstack/nova-specs/blob/master/specs/liberty/implemented/metadata-service-network-info.rst
567
+ def host_network_data(host)
568
+ p_if = host.primary_interface
569
+ network_data = { links: [], networks: [], services: [] }
570
+ network = { id: 'network0', routes: [] }
571
+ if p_if.subnet
572
+ sn = p_if.subnet
573
+ network[:ip_address] = p_if.ip unless p_if.ip.empty?
574
+ network[:type] = sn.boot_mode == 'DHCP' ? 'ipv4_dhcp' : 'ipv4'
575
+ end
576
+ if p_if.subnet6
577
+ sn = p_if.subnet6
578
+ network[:ip_address] = p_if.ip6 unless p_if.ip6.empty?
579
+ network[:type] = sn.boot_mode == 'DHCP' ? 'ipv6_dhcp' : 'ipv6'
580
+ end
581
+ link = { type: 'phy' }
582
+ link[:id] = p_if.name.empty? ? 'eth0' : p_if.identifier
583
+ link[:name] = link[:id]
584
+ link[:mtu] = sn.mtu
585
+ link[:ethernet_mac_address] = p_if.mac unless p_if.mac.empty?
586
+ network_data[:links] << link
587
+ network[:netmask] = sn.mask unless sn.mask.empty?
588
+ network[:link] = link[:id]
589
+ route = { network: '0.0.0.0', netmask: '0.0.0.0' }
590
+ route[:gateway] = sn.gateway unless sn.gateway.empty?
591
+ network[:routes] << route
592
+ network_data[:networks] << network
593
+ unless sn.dns_primary.empty?
594
+ dns1 = { type: 'dns', address: sn.dns_primary }
595
+ network_data[:services] << dns1
596
+ end
597
+ unless sn.dns_secondary.empty?
598
+ dns2 = { type: 'dns', address: sn.dns_secondary }
599
+ network_data[:services] << dns2
600
+ end
601
+ network_data
602
+ end
603
+
604
+ def add_mac_to_network_data(network_data, vm)
605
+ network_data[:links][0][:ethernet_mac_address] = vm.interfaces.first.mac unless network_data[:links][0][:ethernet_mac_address]
606
+ network_data
607
+ end
608
+
609
+ def generate_configdrive(vm_uuid, meta_data, network_data, user_data, dst_dir)
610
+ Dir.mktmpdir('foreman-configdrive') do |wd|
611
+ iso_file_name = "foreman-configdrive-#{vm_uuid}.iso"
612
+ iso_file_path = File.join(wd, iso_file_name)
613
+ config_dir = FileUtils.mkdir_p(File.join(wd, 'openstack/latest')).first
614
+ meta_data_path = File.join(config_dir, 'meta_data.json')
615
+ user_data_path = File.join(config_dir, 'user_data')
616
+ network_data_path = File.join(config_dir, 'network_data.json')
617
+ File.write(meta_data_path, meta_data)
618
+ File.write(user_data_path, user_data)
619
+ File.write(network_data_path, network_data)
620
+
621
+ cmd = ['/usr/bin/genisoimage', '-output', iso_file_path,
622
+ '-volid', 'config-2', '-joliet', '-rock', wd]
623
+
624
+ raise ::Foreman::Exception, N_('ISO build failed, is the genisoimage package installed?') unless system(*cmd)
625
+
626
+ FileUtils.cp(iso_file_path, dst_dir)
627
+
628
+ return iso_file_name
629
+ end
630
+ end
631
+
632
+ def rescan_iso_libraries
633
+ iso_libraries.each do |sr|
634
+ client.scan_sr sr.reference
635
+ end
636
+ end
637
+
638
+ def iso_libraries
639
+ client.storage_repositories.select do |sr|
640
+ sr.type == 'iso'
641
+ end
642
+ end
643
+
421
644
  def get_templates(templates)
422
645
  tmps = templates.reject(&:is_a_snapshot)
423
646
  tmps.sort_by(&:name)