foreman_hyperv 0.0.4 → 0.1.1

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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1451 -12
  3. data/README.md +2 -0
  4. data/app/assets/javascripts/foreman_hyperv/compute_resource_base.js +78 -0
  5. data/app/helpers/foreman_hyperv/compute_resources_vms_helper.rb +23 -0
  6. data/app/models/concerns/fog_extensions/hyperv/compute.rb +8 -0
  7. data/app/models/concerns/fog_extensions/hyperv/hard_drive.rb +39 -0
  8. data/app/models/concerns/fog_extensions/hyperv/network_adapter.rb +115 -16
  9. data/app/models/concerns/fog_extensions/hyperv/server.rb +95 -39
  10. data/app/models/concerns/foreman_hyperv/host_managed_extensions.rb +65 -0
  11. data/app/models/foreman_hyperv/hyperv.rb +374 -143
  12. data/app/views/compute_resources/form/_hyperv.html.erb +6 -2
  13. data/app/views/compute_resources_vms/form/hyperv/_base.html.erb +36 -25
  14. data/app/views/compute_resources_vms/form/hyperv/_network.html.erb +34 -11
  15. data/app/views/compute_resources_vms/form/hyperv/_volume.html.erb +5 -2
  16. data/app/views/compute_resources_vms/index/_hyperv.html.erb +2 -2
  17. data/app/views/compute_resources_vms/show/_hyperv.html.erb +27 -9
  18. data/lib/foreman_hyperv/engine.rb +38 -17
  19. data/lib/foreman_hyperv/version.rb +3 -1
  20. data/lib/foreman_hyperv.rb +2 -0
  21. data/lib/tasks/foreman_hyperv_tasks.rake +33 -0
  22. metadata +54 -38
  23. data/.gitignore +0 -9
  24. data/.rubocop.yml +0 -22
  25. data/.rubocop_todo.yml +0 -20
  26. data/.travis.yml +0 -5
  27. data/CHANGELOG.md +0 -25
  28. data/Gemfile +0 -6
  29. data/app/assets/javascripts/compute_resources/hyperv/base.js +0 -32
  30. data/app/models/concerns/fog_extensions/hyperv/vhd.rb +0 -12
  31. data/foreman_hyperv.gemspec +0 -27
  32. data/test/foreman_hyperv_test.rb +0 -11
  33. data/test/test_helper.rb +0 -4
data/README.md CHANGED
@@ -10,6 +10,8 @@ Uses the in-development `fog-hyperv` gem found [here](https://github.com/ananace
10
10
 
11
11
  Currently the plugin only supports Hyper-V hosts where the names are well defined in DNS and in connection strings, avoid using IP addresses for now.
12
12
 
13
+ If you're using SELinux, you may need to enable the connect_all boolean. For Foreman 2.0 and earlier, run `setsebool -P passenger_can_connect_all 1`. For Foreman 2.1 and later the command would be `setsebool -P foreman_rails_can_connect_all 1`.
14
+
13
15
  ## Testing/Installing
14
16
 
15
17
  Follow the Foreman manual for [advanced installation from gems](https://theforeman.org/plugins/#2.3AdvancedInstallationfromGems) for `fog-hyperv` and `foreman_hyperv`.
@@ -0,0 +1,78 @@
1
+ $(document).on('ContentLoad', function() { tfm.numFields.initAll(); });
2
+
3
+ function hypervGenerationChange(item) {
4
+ var toIter = ['#host_compute_attributes_secure_boot_enabled', '#compute_attribute_vm_attrs_secure_boot_enabled'];
5
+ gen = $(item).val();
6
+
7
+ if (gen == 'BIOS') {
8
+ for (var i = 0; i < toIter.length; ++i) {
9
+ $(toIter[i]).attr('disabled', true);
10
+ }
11
+ } else {
12
+ for (var i = 0; i < toIter.length; ++i) {
13
+ $(toIter[i]).removeAttr('disabled');
14
+ }
15
+ }
16
+ }
17
+
18
+ function hypervDynamicMemoryChange(item) {
19
+ var toIter = [
20
+ '#host_compute_attributes_memory_maximum',
21
+ '#host_compute_attributes_memory_minimum',
22
+ '#compute_attribute_vm_attrs_memory_maximum',
23
+ '#compute_attribute_vm_attrs_memory_minimum',
24
+ ];
25
+
26
+ if (item.checked) {
27
+ for (var i = 0; i < toIter.length; ++i) {
28
+ $(toIter[i]).removeAttr('disabled');
29
+ }
30
+ } else {
31
+ for (var i = 0; i < toIter.length; ++i) {
32
+ $(toIter[i]).attr('disabled', true);
33
+ }
34
+ }
35
+ }
36
+
37
+ function hypervHostChange(item) {
38
+ console.log('Hyper-V host changed to ' + $(item).val());
39
+
40
+ $('table.hyperv-host-info').hide();
41
+ $('table.hyperv-host-info[data-host="'+$(item).val()+'"]').show();
42
+
43
+ // TODO: Reload available switches
44
+ }
45
+
46
+ function hypervVLANModeChange(item) {
47
+ var parent = [...document.querySelectorAll('fieldset.compute_attributes')].filter(el => el.contains(item))[0];
48
+ if (parent == undefined) {
49
+ parent = [...document.querySelectorAll('div.fields')].filter(el => el.contains(item))[0];
50
+ }
51
+
52
+ $(parent.querySelectorAll('[data-hyperv-vlan-mode]')).hide();
53
+ $(parent.querySelectorAll('[data-hyperv-vlan-mode] input')).attr('disabled', true);
54
+ $(parent.querySelectorAll('[data-hyperv-vlan-mode] select')).attr('disabled', true);
55
+
56
+ var id = '[data-hyperv-vlan-mode="' + $(item).val().toLowerCase() + '"]';
57
+ $(parent.querySelectorAll(id)).show();
58
+ $(parent.querySelectorAll(id + ' input')).removeAttr('disabled');
59
+ $(parent.querySelectorAll(id + ' select')).removeAttr('disabled');
60
+ }
61
+
62
+ function hypervVLANPrivateModeChange(item) {
63
+ var parent = [...document.querySelectorAll('fieldset.compute_attributes')].filter(el => el.contains(item))[0];
64
+ if (parent == undefined) {
65
+ parent = [...document.querySelectorAll('div.fields')].filter(el => el.contains(item))[0];
66
+ }
67
+
68
+ $(parent.querySelectorAll('[data-hyperv-vlan-private-mode]')).hide();
69
+ $(parent.querySelectorAll('[data-hyperv-vlan-private-mode] input')).attr('disabled', true);
70
+
71
+ if ($(item).val() == 'Promiscuous') {
72
+ var id = '[data-hyperv-vlan-private-mode="plural"]';
73
+ } else {
74
+ var id = '[data-hyperv-vlan-private-mode="singular"]';
75
+ }
76
+ $(parent.querySelectorAll(id)).show();
77
+ $(parent.querySelectorAll(id + ' input')).removeAttr('disabled');
78
+ }
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanHyperv
4
+ module ComputeResourcesVmsHelper
5
+ def hyperv_networks(compute_resource)
6
+ compute_resource.switches(nil).map do |sw|
7
+ [sw.id, "#{sw.name}#{" (#{sw.switch_type})" if sw.switch_type}"]
8
+ end
9
+ end
10
+
11
+ def hyperv_generations
12
+ Fog::Hyperv::Compute::Server::VM_GENERATION_VALUES.map { |gen, num| [gen, "Generation #{num} (#{gen})"] }
13
+ end
14
+
15
+ def hyperv_vlan_modes
16
+ Fog::Hyperv::Compute::NetworkAdapterVlan::VLAN_OPERATION_MODE.map { |mode| [mode, mode] }
17
+ end
18
+
19
+ def hyperv_private_vlan_modes
20
+ Fog::Hyperv::Compute::NetworkAdapterVlan::PRIVATE_VLAN_MODE.reject { |mode| mode == :Unknown }.map { |mode| [mode, mode] }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FogExtensions
4
+ module Hyperv
5
+ module Compute
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FogExtensions
4
+ module Hyperv
5
+ module HardDrive
6
+ extend ActiveSupport::Concern
7
+ include ActionView::Helpers::NumberHelper
8
+
9
+ def size_bytes=(new_size)
10
+ raise ArgumentError, "Can't modify a physical disk" if disk
11
+
12
+ self.vhd ||= Fog::Hyperv::Compute::Vhd.new unless persisted?
13
+ vhd.size = new_size
14
+ end
15
+
16
+ def basename
17
+ vhd&.basename
18
+ end
19
+
20
+ def basename=(new_basename)
21
+ raise ArgumentError, "Can't modify a physical disk" if disk
22
+
23
+ self.vhd ||= Fog::Hyperv::Compute::Vhd.new unless persisted?
24
+ vhd.basename = new_basename
25
+ end
26
+
27
+ def compute_attributes
28
+ attributes
29
+ .slice(:id)
30
+ .merge(
31
+ {
32
+ basename: basename,
33
+ size_bytes: size_bytes
34
+ }.compact
35
+ )
36
+ end
37
+ end
38
+ end
39
+ end
@@ -1,36 +1,135 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FogExtensions
2
4
  module Hyperv
3
5
  module NetworkAdapter
4
6
  extend ActiveSupport::Concern
5
- include ActionView::Helpers::NumberHelper
6
-
7
- def to_s
8
- name
9
- end
10
7
 
11
8
  def mac
12
- m = mac_address
13
- "#{m[0, 2]}:#{m[2, 2]}:#{m[4, 2]}:#{m[6, 2]}:#{m[8, 2]}:#{m[10, 2]}"
9
+ return unless mac_address
10
+ return if mac_address.to_i(16).zero?
11
+
12
+ # Downcase and split every 2 chars, join with :
13
+ mac_address.downcase.scan(/.{2}/).join(':')
14
14
  end
15
15
 
16
16
  def mac=(m)
17
- self.mac_address = m.remove ':'
17
+ mac_address = m&.upcase&.delete(':')
18
+ mac_address = Fog::Hyperv::Compute::NetworkAdapter::NIC_FALLBACK_MAC if mac_address.nil? || mac_address.blank?
19
+ mac_address.to_i(16)
20
+ 0
21
+ end
22
+
23
+ # VLAN settings
24
+
25
+ def vlan_operation_mode
26
+ vlan_setting.operation_mode
18
27
  end
19
28
 
20
- def network
21
- switch_name
29
+ def vlan_operation_mode=(mode)
30
+ vlan_setting.operation_mode = mode
31
+ end
32
+
33
+ def vlan_private_mode
34
+ return nil if vlan_setting.private_vlan_mode == :Unknown
35
+
36
+ vlan_setting.private_vlan_mode
22
37
  end
23
38
 
24
- def network=(net)
25
- self.switch_name = net
39
+ def vlan_private_mode=(mode)
40
+ vlan_setting.private_vlan_mode = mode
26
41
  end
27
42
 
28
- def type
29
- is_legacy
43
+ def access_vlan_id
44
+ return nil if vlan_setting.access_vlan_id.zero?
45
+
46
+ vlan_setting.access_vlan_id
47
+ end
48
+
49
+ delegate :access_vlan_id=, to: :vlan_setting
50
+
51
+ def native_vlan_id
52
+ return nil if vlan_setting.native_vlan_id.zero?
53
+
54
+ vlan_setting.native_vlan_id
55
+ end
56
+
57
+ delegate :native_vlan_id=, to: :vlan_setting
58
+
59
+ def allowed_vlan_ids
60
+ return nil unless vlan_setting.allowed_vlan_id_list&.any?
61
+
62
+ Fog::Hyperv::Compute::NetworkAdapterVlan.render_vlan_list vlan_setting.allowed_vlan_id_list
30
63
  end
31
64
 
32
- def type=(type)
33
- self.is_legacy = type
65
+ def allowed_vlan_ids=(ids)
66
+ ids ||= ''
67
+ vlan_setting.allowed_vlan_id_list = parse_vlan_list(ids)
68
+ end
69
+
70
+ def primary_vlan_id
71
+ return nil if vlan_setting.primary_vlan_id.zero?
72
+
73
+ vlan_setting.primary_vlan_id
74
+ end
75
+
76
+ delegate :primary_vlan_id=, to: :vlan_setting
77
+
78
+ def secondary_vlan_id
79
+ return nil if vlan_setting.secondary_vlan_id.zero?
80
+
81
+ vlan_setting.secondary_vlan_id
82
+ end
83
+
84
+ delegate :secondary_vlan_id=, to: :vlan_setting
85
+
86
+ def secondary_vlan_ids
87
+ return nil unless vlan_setting.secondary_vlan_id_list&.any?
88
+
89
+ Fog::Hyperv::Compute::NetworkAdapterVlan.render_vlan_list vlan_setting.secondary_vlan_id_list
90
+ end
91
+
92
+ def secondary_vlan_ids=(ids)
93
+ ids ||= ''
94
+ vlan_setting.secondary_vlan_id_list = parse_vlan_list(ids)
95
+ end
96
+
97
+ def compute_attributes
98
+ attributes
99
+ .slice(
100
+ :id,
101
+ :switch_id
102
+ )
103
+ .merge(
104
+ vlan_setting.attributes.slice(
105
+ :vlan_operation_mode,
106
+ :vlan_private_mode,
107
+ :access_vlan_id,
108
+ :native_vlan_id,
109
+ :primary_vlan_id,
110
+ :secondary_vlan_id
111
+ )
112
+ )
113
+ .merge(
114
+ secondary_vlan_ids: secondary_vlan_ids,
115
+ allowed_vlan_ids: allowed_vlan_ids
116
+ )
117
+ .compact
118
+ end
119
+
120
+ private
121
+
122
+ def parse_vlan_list(list)
123
+ ret = []
124
+ list.split(',').map do |num|
125
+ if num.include? '-'
126
+ rstart, rend = num.split('-')
127
+ ret += (rstart.to_i..rend.to_i).to_a
128
+ else
129
+ ret << num.to_i
130
+ end
131
+ end
132
+ ret.sort.uniq
34
133
  end
35
134
  end
36
135
  end
@@ -1,25 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module FogExtensions
2
4
  module Hyperv
3
5
  module Server
4
6
  extend ActiveSupport::Concern
5
7
  include ActionView::Helpers::NumberHelper
6
8
 
7
- attr_accessor :start
8
-
9
9
  def to_s
10
10
  name
11
11
  end
12
12
 
13
13
  def folder_name
14
- name.gsub(/[^0-9A-Za-z.\-]/, '_')
15
- end
16
-
17
- def mac(m = mac_addresses.first)
18
- "#{m[0, 2]}:#{m[2, 2]}:#{m[4, 2]}:#{m[6, 2]}:#{m[8, 2]}:#{m[10, 2]}".downcase
14
+ name.gsub(/[^0-9A-Za-z.-]/, '_')
19
15
  end
20
16
 
21
- def clean_mac_addresses
22
- network_adapters.map { |n| mac(n.mac_address) }
17
+ def mac
18
+ network_adapters.first.mac
23
19
  end
24
20
 
25
21
  def interfaces
@@ -27,24 +23,53 @@ module FogExtensions
27
23
  end
28
24
 
29
25
  def volumes
30
- vhds
26
+ hard_drives
31
27
  end
32
28
 
33
- def persisted?
34
- identity.present?
29
+ def interfaces_attributes=(_attributes); end
30
+
31
+ def volumes_attributes=(_attributes); end
32
+
33
+ # Override fog configuration with explicit cluster_name handling
34
+ def cluster
35
+ @cluster
35
36
  end
36
37
 
37
- def interfaces_attributes=(_attributes)
38
- true
38
+ def cluster_name
39
+ cluster&.name
39
40
  end
40
41
 
41
- def volumes_attributes=(_attributes); end
42
+ def cluster_name=(name)
43
+ @cluster = service.clusters.get(name)
44
+ end
45
+
46
+ def vlan
47
+ nic = network_adapters.first
42
48
 
43
- def vlan; end
49
+ nic.access_vlan_id || nic.native_vlan_id || nic.primary_vlan_id
50
+ end
44
51
 
45
- def vlan=(_vlan); end
52
+ def vlan=(vlan)
53
+ logger.warn "using vlan=#{vlan.inspect} on Hyper-V VM, this can lead to unexpected results"
54
+ nic = network_adapters.first
55
+ if vlan.present? && vlan.to_i.positive?
56
+ nic.vlan_operation_mode = :Access if nic.vlan_operation_mode == :Untagged
57
+ case nic.vlan_operation_mode
58
+ when :Access
59
+ nic.access_vlan_id = vlan
60
+ when :Trunk
61
+ nic.native_vlan_id = vlan
62
+ when :Private
63
+ nic.primary_vlan_id = vlan
64
+ end
65
+ else
66
+ nic.vlan_operation_mode = :Untagged
67
+ end
68
+ end
46
69
 
47
70
  def secure_boot_enabled=(enabled)
71
+ return if generation != :UEFI
72
+
48
73
  @secure_boot = enabled
49
74
  return unless persisted?
50
75
 
@@ -52,41 +77,72 @@ module FogExtensions
52
77
  end
53
78
 
54
79
  def secure_boot_enabled
55
- return false if generation == 1
80
+ return false if generation != :UEFI
56
81
  return @secure_boot unless persisted?
57
82
 
58
83
  firmware.secure_boot == :On
59
84
  end
60
85
 
61
- def reset
62
- restart(force: true)
63
- end
64
-
65
- def state
66
- attributes[:state].to_s.upcase
67
- end
68
-
69
- def stop
70
- requires :name, :computer_name
71
- service.stop_vm options.merge(
72
- name: name,
73
- computer_name: computer_name,
74
- force: true
75
- )
76
- end
77
-
78
86
  def vm_description
79
87
  format _('%{cpus} CPUs and %{ram} memory'),
80
88
  cpus: processor_count,
81
89
  ram: number_to_human_size(memory_startup)
82
90
  end
83
91
 
92
+ def compute_attributes
93
+ attributes.merge(
94
+ interfaces_attributes: vm.network_adapters.each_with_index.to_h { |nic, idx| [idx, nic.compute_attributes] },
95
+ volumes_attributes: vm.hard_drives.each_with_index.to_h { |hdd, idx| [idx, hdd.compute_attributes] }
96
+ )
97
+ end
98
+
84
99
  def select_nic(fog_nics, nic)
85
100
  nic_attrs = nic.compute_attributes
86
- logger.debug "select_nic(#{fog_nics}, #{nic}[#{nic_attrs}])"
87
- match = fog_nics.detect { |fn| fn.id == nic_attrs['id'] } # Check the id
88
- match ||= fog_nics.detect { |fn| fn.name == nic_attrs['name'] } # Check the name
89
- match ||= fog_nics.detect { |fn| fn.switch_name == nic_attrs['network'] } # Fall back to the switch
101
+
102
+ # Match for exact data
103
+ match = fog_nics.detect { |fn| fn.id == nic_attrs['id'].presence }
104
+ match ||= fog_nics.detect { |fn| fn.mac == nic.mac }
105
+ # match ||= fog_nics.detect { |fn| fn.name == nic_attrs['name'].presence }
106
+
107
+ unless match
108
+ # Match on networking, limit potentials down to identical configuration and then pick the first
109
+ potential = fog_nics.select do |fn|
110
+ fn.switch_id == nic_attrs['switch_id'].presence || fn.switch_name == nic_attrs['switch_name'].presence
111
+ end
112
+ potential.select! { |fn| fn.vlan_operation_mode.to_s == nic_attrs['vlan_operation_mode'] }
113
+ case nic_attrs['vlan_operation_mode']
114
+ when 'Access'
115
+ potential.select! { |fn| fn.access_vlan_id.to_s == nic_attrs['access_vlan_id'] }
116
+ when 'Trunk'
117
+ potential.select! { |fn| fn.native_vlan_id.to_s == nic_attrs['native_vlan_id'] }
118
+ if nic_attrs['allowed_vlan_ids'].present?
119
+ potential.select! do |fn|
120
+ fn.allowed_vlan_ids.split(',').map(&:strip) == nic_attrs['allowed_vlan_ids'].split(',').map(&:strip)
121
+ end
122
+ end
123
+ when 'Private'
124
+ potential.select! { |fn| fn.vlan_private_mode.to_s == nic_attrs['vlan_private_mode'] }
125
+ potential.select! { |fn| fn.primary_vlan_id.to_s == nic_attrs['primary_vlan_id'].to_s } \
126
+ if nic_attrs['primary_vlan_id'].present?
127
+
128
+ if nic_attrs['vlan_private_mode'] == 'Promiscuous'
129
+ if nic_attrs['secondary_vlan_ids'].present?
130
+ potential.select! do |fn|
131
+ fn.secondary_vlan_ids.split(',').map(&:strip) == nic_attrs['secondary_vlan_ids'].split(',').map(&:strip)
132
+ end
133
+ end
134
+ elsif nic_attrs['secondary_vlan_id'].present?
135
+ potential.select! { |fn| fn.secondary_vlan_id.to_s == nic_attrs['secondary_vlan_id'].to_s }
136
+ end
137
+ end
138
+
139
+ match = potential.first
140
+ end
141
+ return unless match
142
+
143
+ # Store Hyper-V ID in compute attributes
144
+ nic.compute_attributes['identity'] = match.id
145
+
90
146
  match
91
147
  end
92
148
  end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ForemanHyperv
4
+ module HostManagedExtensions
5
+ extend ActiveSupport::Concern
6
+
7
+ def update(attributes = {})
8
+ hyperv_add_attributes(attributes) if provider == 'Hyper-V' && attributes.key?('compute_attributes')
9
+
10
+ super
11
+ end
12
+
13
+ def setComputeUpdate
14
+ ret = super
15
+ return ret unless provider == 'Hyper-V'
16
+
17
+ begin
18
+ hyperv_sync_interfaces
19
+ rescue StandardError => e
20
+ failure _("Failed to update a compute %{compute_resource} instance %{name}: %{e}") %
21
+ { compute_resource: compute_resource, name: name, e: e }, e
22
+ end
23
+ true
24
+ end
25
+
26
+ private
27
+
28
+ # Reconcile the mapping between Hyper-V and Foreman interfaces
29
+ def hyperv_sync_interfaces
30
+ unmapped_ifaces = interfaces.select { |iface| iface.physical? && !iface.compute_attributes['identity'].present? }
31
+ return true if unmapped_ifaces.empty?
32
+
33
+ logger.info "Mapping #{unmapped_ifaces.count} unknown interfaces for #{name}"
34
+
35
+ self.vm = compute_object
36
+ fog_nics = vm.interfaces.dup
37
+ interfaces.each { |iface| fog_nics.delete_if { |vmiface| vmiface.id == iface.compute_attributes['identity'] } }
38
+
39
+ unmapped_ifaces.each do |nic|
40
+ logger.debug "Matching #{nic.inspect} against #{fog_nics}"
41
+ selected_nic = vm.select_nic(fog_nics, nic)
42
+ if selected_nic.nil?
43
+ logger.warn "Orchestration::Compute: Could not match network interface #{nic.inspect}"
44
+ raise ArgumentError, _("Could not find virtual machine network interface matching %s") %
45
+ [nic.identifier, nic.ip, nic.name, nic.type].find(&:present?)
46
+ end
47
+
48
+ logger.debug "Orchestration::Compute: nic #{nic.inspect} assigned to #{selected_nic.inspect}"
49
+ nic.mac ||= selected_nic.mac
50
+ nic.save
51
+ fog_nics.delete selected_nic
52
+ end
53
+ end
54
+
55
+ # Inject interface attributes into the compute values, to allow modifying Hyper-V configuration on hosts
56
+ def hyperv_add_attributes(attributes)
57
+ compute = attributes['compute_attributes'] ||= {}
58
+ compute['interfaces_attributes'] ||= {}
59
+
60
+ attributes['interfaces_attributes'].each do |idx, interface|
61
+ compute['interfaces_attributes'][idx] = interface
62
+ end
63
+ end
64
+ end
65
+ end