foreman_hyperv 0.0.3 → 0.1.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.
Files changed (33) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE.txt +1451 -12
  3. data/README.md +8 -2
  4. data/app/assets/javascripts/foreman_hyperv/compute_resource_base.js +78 -0
  5. data/app/helpers/foreman_hyperv/compute_resources_vms_helper.rb +19 -0
  6. data/app/models/concerns/fog_extensions/hyperv/compute.rb +6 -0
  7. data/app/models/concerns/fog_extensions/hyperv/hard_drive.rb +37 -0
  8. data/app/models/concerns/fog_extensions/hyperv/network_adapter.rb +115 -12
  9. data/app/models/concerns/fog_extensions/hyperv/server.rb +90 -31
  10. data/app/models/concerns/foreman_hyperv/host_managed_extensions.rb +66 -0
  11. data/app/models/foreman_hyperv/hyperv.rb +366 -131
  12. data/app/views/compute_resources/form/_hyperv.html.erb +6 -2
  13. data/app/views/compute_resources_vms/form/hyperv/_base.html.erb +53 -21
  14. data/app/views/compute_resources_vms/form/hyperv/_network.html.erb +34 -10
  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 +25 -37
  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 -17
  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
@@ -4,7 +4,13 @@
4
4
 
5
5
  Microsoft Hyper-V compute resource for Foreman
6
6
 
7
- Uses the in-development `fog-hyperv` gem found [here](https://github.com/ace13/fog-hyperv).
7
+ Uses the in-development `fog-hyperv` gem found [here](https://github.com/ananace/fog-hyperv).
8
+
9
+ ## Nota Bene
10
+
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
+
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`.
8
14
 
9
15
  ## Testing/Installing
10
16
 
@@ -16,7 +22,7 @@ Do bear in mind that this is still very early in development, so plenty of issue
16
22
 
17
23
  ## Contributing
18
24
 
19
- Bug reports and pull requests are welcome on GitHub at https://github.com/ace13/foreman_hyperv.
25
+ Bug reports and pull requests are welcome on GitHub at https://github.com/ananace/foreman_hyperv.
20
26
 
21
27
  ## License
22
28
 
@@ -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,19 @@
1
+ module ForemanHyperv
2
+ module ComputeResourcesVmsHelper
3
+ def hyperv_networks(compute_resource)
4
+ compute_resource.switches(nil).map do |sw|
5
+ [ sw.id, "#{sw.name}#{sw.switch_type ? " (#{sw.switch_type})" : nil}" ]
6
+ end
7
+ end
8
+
9
+ def hyperv_generations
10
+ Fog::Hyperv::Compute::Server::VM_GENERATION_VALUES.map { |gen, num| [gen, "Generation #{num} (#{gen})"] }
11
+ end
12
+ def hyperv_vlan_modes
13
+ Fog::Hyperv::Compute::NetworkAdapterVlan::VLAN_OPERATION_MODE.map { |mode| [mode, mode] }
14
+ end
15
+ def hyperv_private_vlan_modes
16
+ Fog::Hyperv::Compute::NetworkAdapterVlan::PRIVATE_VLAN_MODE.reject { |mode| mode == :Unknown }.map { |mode| [mode, mode] }
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,6 @@
1
+ module FogExtensions
2
+ module Hyperv
3
+ module Compute
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,37 @@
1
+ module FogExtensions
2
+ module Hyperv
3
+ module HardDrive
4
+ extend ActiveSupport::Concern
5
+ include ActionView::Helpers::NumberHelper
6
+
7
+ def size_bytes=(new_size)
8
+ raise ArgumentError, "Can't modify a physical disk" if disk
9
+
10
+ self.vhd ||= Fog::Hyperv::Compute::Vhd.new unless persisted?
11
+ vhd.size = new_size
12
+ end
13
+
14
+ def basename
15
+ vhd&.basename
16
+ end
17
+
18
+ def basename=(new_basename)
19
+ raise ArgumentError, "Can't modify a physical disk" if disk
20
+
21
+ self.vhd ||= Fog::Hyperv::Compute::Vhd.new unless persisted?
22
+ vhd.basename = new_basename
23
+ end
24
+
25
+ def compute_attributes
26
+ attributes
27
+ .slice(:id)
28
+ .merge(
29
+ {
30
+ basename:,
31
+ size_bytes:
32
+ }.compact
33
+ )
34
+ end
35
+ end
36
+ end
37
+ end
@@ -1,28 +1,131 @@
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) == 0
11
+
12
+ # Downcase and split every 2 chars, join with :
13
+ mac_address.downcase.scan(%r{.{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
+ dynamic_mac_address_enabled = (mac_address.to_i(16) == 0)
20
+ end
21
+
22
+ # VLAN settings
23
+
24
+ def vlan_operation_mode
25
+ vlan_setting.operation_mode
26
+ end
27
+ def vlan_operation_mode=(mode)
28
+ vlan_setting.operation_mode = mode
18
29
  end
19
30
 
20
- def network
21
- switch_name
31
+ def vlan_private_mode
32
+ return nil if vlan_setting.private_vlan_mode == :Unknown
33
+
34
+ vlan_setting.private_vlan_mode
35
+ end
36
+ def vlan_private_mode=(mode)
37
+ vlan_setting.private_vlan_mode = mode
22
38
  end
23
39
 
24
- def network=(net)
25
- self.switch_name = net
40
+ def access_vlan_id
41
+ return nil if vlan_setting.access_vlan_id.zero?
42
+
43
+ vlan_setting.access_vlan_id
44
+ end
45
+ def access_vlan_id=(id)
46
+ vlan_setting.access_vlan_id = id
47
+ end
48
+
49
+ def native_vlan_id
50
+ return nil if vlan_setting.native_vlan_id.zero?
51
+
52
+ vlan_setting.native_vlan_id
53
+ end
54
+ def native_vlan_id=(id)
55
+ vlan_setting.native_vlan_id = id
56
+ end
57
+
58
+ def allowed_vlan_ids
59
+ return nil unless vlan_setting.allowed_vlan_id_list&.any?
60
+
61
+ Fog::Hyperv::Compute::NetworkAdapterVlan.render_vlan_list vlan_setting.allowed_vlan_id_list
62
+ end
63
+ def allowed_vlan_ids=(ids)
64
+ ids ||= ''
65
+ vlan_setting.allowed_vlan_id_list = parse_vlan_list(ids)
66
+ end
67
+
68
+ def primary_vlan_id
69
+ return nil if vlan_setting.primary_vlan_id.zero?
70
+
71
+ vlan_setting.primary_vlan_id
72
+ end
73
+ def primary_vlan_id=(id)
74
+ vlan_setting.primary_vlan_id = id
75
+ end
76
+
77
+ def secondary_vlan_id
78
+ return nil if vlan_setting.secondary_vlan_id.zero?
79
+
80
+ vlan_setting.secondary_vlan_id
81
+ end
82
+ def secondary_vlan_id=(id)
83
+ vlan_setting.secondary_vlan_id = id
84
+ end
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
+ def secondary_vlan_ids=(ids)
92
+ ids ||= ''
93
+ vlan_setting.secondary_vlan_id_list = parse_vlan_list(ids)
94
+ end
95
+
96
+ def compute_attributes
97
+ attributes
98
+ .slice(
99
+ :id,
100
+ :switch_id
101
+ )
102
+ .merge(
103
+ {
104
+ vlan_operation_mode:,
105
+ vlan_private_mode:,
106
+ access_vlan_id:,
107
+ native_vlan_id:,
108
+ allowed_vlan_ids:,
109
+ primary_vlan_id:,
110
+ secondary_vlan_id:,
111
+ secondary_vlan_ids:
112
+ }.compact
113
+ )
114
+ end
115
+
116
+ private
117
+
118
+ def parse_vlan_list(list)
119
+ ret = []
120
+ list.split(',').map do |num|
121
+ if num.include? '-'
122
+ rstart, rend = num.split('-')
123
+ ret += (rstart.to_i..rend.to_i).to_a
124
+ else
125
+ ret << num.to_i
126
+ end
127
+ end
128
+ ret.sort.uniq
26
129
  end
27
130
  end
28
131
  end
@@ -4,8 +4,6 @@ module FogExtensions
4
4
  extend ActiveSupport::Concern
5
5
  include ActionView::Helpers::NumberHelper
6
6
 
7
- attr_accessor :start
8
-
9
7
  def to_s
10
8
  name
11
9
  end
@@ -14,12 +12,8 @@ module FogExtensions
14
12
  name.gsub(/[^0-9A-Za-z.\-]/, '_')
15
13
  end
16
14
 
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
19
- end
20
-
21
- def clean_mac_addresses
22
- network_adapters.map { |n| mac(n.mac_address) }
15
+ def mac
16
+ network_adapters.first.mac
23
17
  end
24
18
 
25
19
  def interfaces
@@ -27,18 +21,54 @@ module FogExtensions
27
21
  end
28
22
 
29
23
  def volumes
30
- vhds
31
- end
32
-
33
- def persisted?
34
- identity.present?
24
+ hard_drives
35
25
  end
36
26
 
37
27
  def interfaces_attributes=(_attributes); end
38
28
 
39
29
  def volumes_attributes=(_attributes); end
40
30
 
31
+ # Override fog configuration with explicit cluster_name handling
32
+ def cluster
33
+ @cluster
34
+ end
35
+
36
+ def cluster_name
37
+ cluster&.name
38
+ end
39
+
40
+ def cluster_name=(name)
41
+ @cluster = service.clusters.get(name)
42
+ end
43
+ #
44
+
45
+ def vlan
46
+ nic = network_adapters.first
47
+
48
+ nic.access_vlan_id || nic.native_vlan_id || nic.primary_vlan_id
49
+ end
50
+
51
+ def vlan=(vlan)
52
+ logger.warn "using vlan=#{vlan.inspect} on Hyper-V VM, this can lead to unexpected results"
53
+ nic = network_adapters.first
54
+ if vlan.present? && vlan.to_i > 0
55
+ nic.vlan_operation_mode = :Access if nic.vlan_operation_mode == :Untagged
56
+ case nic.vlan_operation_mode
57
+ when :Access
58
+ nic.access_vlan_id = vlan
59
+ when :Trunk
60
+ nic.native_vlan_id = vlan
61
+ when :Private
62
+ nic.primary_vlan_id = vlan
63
+ end
64
+ else
65
+ nic.vlan_operation_mode = :Untagged
66
+ end
67
+ end
68
+
41
69
  def secure_boot_enabled=(enabled)
70
+ return if generation != :UEFI
71
+
42
72
  @secure_boot = enabled
43
73
  return unless persisted?
44
74
 
@@ -46,37 +76,66 @@ module FogExtensions
46
76
  end
47
77
 
48
78
  def secure_boot_enabled
49
- return false if generation == 1
79
+ return false if generation != :UEFI
50
80
  return @secure_boot unless persisted?
51
81
 
52
82
  firmware.secure_boot == :On
53
83
  end
54
84
 
55
- def reset
56
- restart(force: true)
57
- end
58
-
59
- def stop
60
- requires :name, :computer_name
61
- service.stop_vm options.merge(
62
- name: name,
63
- computer_name: computer_name,
64
- force: true
65
- )
66
- end
67
-
68
85
  def vm_description
69
86
  format _('%{cpus} CPUs and %{ram} memory'),
70
87
  cpus: processor_count,
71
88
  ram: number_to_human_size(memory_startup)
72
89
  end
73
90
 
91
+ def compute_attributes
92
+ attributes.merge(
93
+ interfaces_attributes: vm.network_adapters.each_with_index.to_h { |nic, idx| [idx, nic.compute_attributes] },
94
+ volumes_attributes: vm.hard_drives.each_with_index.to_h { |hdd, idx| [idx, hdd.compute_attributes] }
95
+ )
96
+ end
97
+
74
98
  def select_nic(fog_nics, nic)
75
99
  nic_attrs = nic.compute_attributes
76
- logger.debug "select_nic(#{fog_nics}, #{nic}[#{nic_attrs}])"
77
- match = fog_nics.detect { |fn| fn.id == nic_attrs['id'] } # Check the id
78
- match ||= fog_nics.detect { |fn| fn.name == nic_attrs['name'] } # Check the name
79
- match ||= fog_nics.detect { |fn| fn.switch_name == nic_attrs['network'] } # Fall back to the switch
100
+
101
+ # Match for exact data
102
+ match = fog_nics.detect { |fn| fn.id == nic_attrs['id'].presence }
103
+ match ||= fog_nics.detect { |fn| fn.mac == nic.mac }
104
+ # match ||= fog_nics.detect { |fn| fn.name == nic_attrs['name'].presence }
105
+
106
+ if !match
107
+ # Match on networking, limit potentials down to identical configuration and then pick the first
108
+ potential = fog_nics.select do |fn|
109
+ fn.switch_id == nic_attrs['switch_id'].presence || fn.switch_name == nic_attrs['switch_name'].presence
110
+ end
111
+ potential.select! { |fn| fn.vlan_operation_mode.to_s == nic_attrs['vlan_operation_mode'] }
112
+ if nic_attrs['vlan_operation_mode'] == 'Access'
113
+ potential.select! { |fn| fn.access_vlan_id.to_s == nic_attrs['access_vlan_id'] }
114
+ elsif nic_attrs['vlan_operation_mode'] == 'Trunk'
115
+ potential.select! { |fn| fn.native_vlan_id.to_s == nic_attrs['native_vlan_id'] }
116
+ potential.select! { |fn| fn.allowed_vlan_ids.split(',').map(&:strip) == nic_attrs['allowed_vlan_ids'].split(',').map(&:strip) } \
117
+ if nic_attrs['allowed_vlan_ids'].present?
118
+ elsif nic_attrs['vlan_operation_mode'] == 'Private'
119
+ potential.select! { |fn| fn.vlan_private_mode.to_s == nic_attrs['vlan_private_mode'] }
120
+ potential.select! { |fn| fn.primary_vlan_id.to_s == nic_attrs['primary_vlan_id'].to_s } \
121
+ if nic_attrs['primary_vlan_id'].present?
122
+
123
+ if nic_attrs['vlan_private_mode'] == 'Promiscuous'
124
+ potential.select! { |fn| fn.secondary_vlan_ids.split(',').map(&:strip) == nic_attrs['secondary_vlan_ids'].split(',').map(&:strip) } \
125
+ if nic_attrs['secondary_vlan_ids'].present?
126
+ else
127
+ potential.select! { |fn| fn.secondary_vlan_id.to_s == nic_attrs['secondary_vlan_id'].to_s } \
128
+ if nic_attrs['secondary_vlan_id'].present?
129
+ end
130
+ end
131
+
132
+ match = potential.first
133
+ end
134
+ return unless match
135
+
136
+ # Store Hyper-V ID in compute attributes
137
+ nic.compute_attributes['identity'] = match.id
138
+
80
139
  match
81
140
  end
82
141
  end
@@ -0,0 +1,66 @@
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 => e
20
+ failure _("Failed to update a compute %{compute_resource} instance %{name}: %{e}") %
21
+ { compute_resource:, name:, 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, \
45
+ _("Could not find virtual machine network interface matching %s") %
46
+ [nic.identifier, nic.ip, nic.name, nic.type].find(&:present?)
47
+ end
48
+
49
+ logger.debug "Orchestration::Compute: nic #{nic.inspect} assigned to #{selected_nic.inspect}"
50
+ nic.mac ||= selected_nic.mac
51
+ nic.save
52
+ fog_nics.delete selected_nic
53
+ end
54
+ end
55
+
56
+ # Inject interface attributes into the compute values, to allow modifying Hyper-V configuration on hosts
57
+ def hyperv_add_attributes(attributes)
58
+ compute = attributes['compute_attributes'] ||= {}
59
+ compute['interfaces_attributes'] ||= {}
60
+
61
+ attributes['interfaces_attributes'].each do |idx, interface|
62
+ compute['interfaces_attributes'][idx] = interface
63
+ end
64
+ end
65
+ end
66
+ end