Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for NVME Controllers #300

Merged
merged 1 commit into from
Jul 11, 2024
Merged

Conversation

girijaasoni
Copy link
Contributor

@girijaasoni girijaasoni commented May 15, 2024

This is the solution for the feature request.
NVME (Non Volatile Memory Express) controllers are a class above the currently supported (SCSI Controllers) and has multiple advantages

Currently Foreman and fog-vsphere are designed to only support SCSI controllers and it's types. The goal is to extend it to adding NVME and in future, other types. Do give this a read SCSI, SATA, and NVMe Storage Controller Conditions, Limitations, and Compatibility

Front End PR : theforeman/foreman#10168
Hammer doc PR : theforeman/hammer-cli-foreman#628

More details: https://docs.google.com/document/d/1hs4JekeCQP6brbZGg8hS8kcHohselH1HTEEhThgw-AE/edit#heading=h.lrnpszeygrmo

Ready for testing :D

Copy link
Contributor

@ekohl ekohl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For RuboCop it's probably best to disable it in that class.

Comment on lines 363 to 366
attributes[:volumes]&.map! do |vol|
return true unless vol.key?(:controller_key)
end
false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why you're using map!. Wouldn't that potentially wipe data? Isn't this what you're looking for?

Suggested change
attributes[:volumes]&.map! do |vol|
return true unless vol.key?(:controller_key)
end
false
attributes[:volumes]&.any? { |vol| !vol.key?(:controller_key) } || false

Technically || false isn't needed since nil and false evaluate to the same, but it ensures a proper boolean is returned.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's a much better approach, Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I recommend reading https://docs.ruby-lang.org/en/master/Enumerable.html because it's extremely useful to know. Many things include Enumerable, making your life easier.

Comment on lines 303 to 305
def nvme_controller
nvme_controllers.first
end
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From reading the above you're adding this, but deprecating it in the same PR. Wouldn't it be better to not introduce it at all?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can just not deprecate it. Scsi controllers were deprecated in 60a5dc3#diff-49106de6ade06895f1a509db7c9cd040ed76bfdb472548d8b99efc0468dce548R9 , not sure why
I tried to mirror the behavior but I guess we can undo that. @chris1984 , any idea why scsi_controllers were deprecated

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note it's the singular scsi_controller that's deprecated. IMHO it makes sense if you can have more than one that there isn't a singular version. Since it was done in 2016, by this point you can probably delete it.

Copy link

@ShimShtein ShimShtein left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also need to add mocked data to

to contain at least one host with NVME controller and a drive that is attached to it.

Comment on lines 12 to 13
self.key ||= 2000
self.type ||= "VirtualNVMEController"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we extract those into constants:

DEFAULT_KEY = 2000
DEFAULT_TYPE = "VirtualNVMEController"

# ...

self.key ||= DEFAULT_KEY
self.type ||= DEFAULT_TYPE

@@ -11,6 +11,7 @@ class SCSIController < Fog::Model
def initialize(attributes = {})
super
self.key ||= 1000
self.type ||= "VirtualLsiLogicController"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be nice to extract to constant too

@@ -7,6 +7,7 @@ class Server < Fog::Compute::Server
extend Fog::Deprecation
deprecate(:ipaddress, :public_ip_address)
deprecate(:scsi_controller, :scsi_controllers)
deprecate(:nvme_controller, :nvme_controllers)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose we can skip creating this method in the first place. If we don't want people to use it - we can skip creating it.

end

def update_controller_key(vol)
if !attributes[:scsi_controllers].empty? && !vol[:controller_key]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I like early returns, this way we are clearly defining a precondition for the rest of the function

return if vol[:controller_key]

And then we can avoid the if statement altogether:

attributes[:scsi_controllers].first&.key || 1000

def initialize_volumes
if attributes[:volumes] && attributes[:volumes].is_a?(Array)
if attributes[:volumes] && attributes[:volumes].is_a?(Array) && !attributes[:volumes].empty?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think precondition here would be also more readable:

return unless attributes[:volumes].is_a?(Array) && !attributes[:volumes].empty?

We can skip the first condition entirely, since:

[4] pry(main)> nil.is_a?(Array)
=> false

attributes[:nvme_controllers].map! do |controller|
controller.is_a?(Hash) ? Fog::Vsphere::Compute::NVMEController.new(controller) : controller
end
elsif attributes[:nvme_controller] && attributes[:nvme_controller].is_a?(Hash)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we decided that nvme_controller should be removed, this part will need to be removed also.

@ShimShtein
Copy link

Not tested, but code-wise looks good

@chris1984
Copy link
Collaborator

Starting to test

Copy link
Contributor

@ekohl ekohl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see the Layout/EmptyLineAfterGuardClause enabled here, but I like the style where you leave an empty line after a return statement.

.rubocop.yml Outdated
Metrics/BlockLength:
Enabled: false

Style/MutableConstant:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please keep this cop enabled

.rubocop.yml Outdated
Comment on lines 46 to 47
Metrics/ClassLength:
Enabled: false

Metrics/BlockLength:
Enabled: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was more thinking about disabling them in the Ruby files where needed rather than globally.

vol.server = self
vol
end
return unless attributes[:volumes].is_a?(Array) && !attributes[:volumes].empty?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd leave out the empty check because calling map on an empty array is essentially a noop.

Suggested change
return unless attributes[:volumes].is_a?(Array) && !attributes[:volumes].empty?
return unless attributes[:volumes].is_a?(Array)

Comment on lines 364 to 365
return if vol[:controller_key]
vol[:controller_key] = attributes[:scsi_controllers].first&.key || 1000
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A more common pattern is:

Suggested change
return if vol[:controller_key]
vol[:controller_key] = attributes[:scsi_controllers].first&.key || 1000
vol[:controller_key] ||= attributes[:scsi_controllers].first&.key || 1000

end

def initialize_nvme_controllers
if attributes[:nvme_controllers] && attributes[:nvme_controllers].is_a?(Array) && !attributes[:nvme_controllers].empty?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is sufficient:

Suggested change
if attributes[:nvme_controllers] && attributes[:nvme_controllers].is_a?(Array) && !attributes[:nvme_controllers].empty?
if attributes[:nvme_controllers].is_a?(Array)

attributes[:nvme_controllers] must be true for attributes[:nvme_controllers].is_a?(Array) to be true because only false and nil are the only values that evaluate to false. Neither false.is_a?(Array) nor nil.is_a?(Array) would pass, so you can drop it.

The empty check is also redundant, because calling map! on an empty array would be a noop.

.rubocop.yml Outdated
Enabled: false

Style/MutableConstant:
Enabled: false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure you keep line endings at the end of files.

@@ -142,6 +142,10 @@ def device_change(attributes)
devices << scsi_controllers.each_with_index.map { |controller, index| create_controller(controller, index) }
end

if (nvme_controllers = (attributes[:nvme_controllers]))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (nvme_controllers = (attributes[:nvme_controllers]))
if (nvme_controllers = attributes[:nvme_controllers])

@@ -255,14 +259,18 @@ def create_controller(controller = nil, index = 0)
operation: options[:operation],
device: controller_class.new(key: options[:key] || (1000 + index),
busNumber: options[:bus_id] || index,
**(options[:type] == RbVmomi::VIM::VirtualAHCIController ? {} : {sharedBus: controller_get_shared_from_options(options)}))
**(check_type(options[:type]) ? {} : {sharedBus: controller_get_shared_from_options(options)}))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check_type is a really generic name. I'd be tempted to write the class as something like:

def create_controller(controller = nil, index = 0)
  # ...
  controller_class = ...
  device_options = {}
  unless [RbVmomi::VIM::VirtualAHCIController, RbVmomi::VIM::VirtualNVMEController, "VirtualNVMEController"].include?(options[:type])
    device_options[:sharedBus] = controller_get_shared_from_options(options)
  end
  {
    operation: options[:operation],
    device: controller_class.new(key: options[:key] || (1000 + index),
                                 busNumber: options[:bus_id] || index,
                                 **device_options)
  }

And then you can consider the following, but now we're really getting in the code refactoring realm.

device_options = {
  key: options.fetch(:key, 1000 + index),
  busNumber: options.fetch(:bus_id, index),
}

# require 'pry'
# binding.pry
ctrl = get_vm_ref(vm_id).config.hardware.device.find { |device| device.is_a?(RbVmomi::VIM::VirtualNVMEController) }
raise(Fog::Vsphere::Compute::NotFound) unless ctrl
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be explicit what wasn't found

Suggested change
raise(Fog::Vsphere::Compute::NotFound) unless ctrl
raise Fog::Vsphere::Compute::NotFound, "No NVME controller found for #{vm_id}" unless ctrl

Comment on lines 6 to 7
# require 'pry'
# binding.pry
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# require 'pry'
# binding.pry

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ready for re-review

@girijaasoni girijaasoni force-pushed the nvme-controllers branch 2 times, most recently from 35135ee to 26303a4 Compare June 24, 2024 06:41
@@ -346,11 +349,11 @@ def is_uuid?(id)
end
end

class Mock
class Mock # rubocop:disable Metrics/ClassLength

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need to reenable the cop after the class

include Shared
# rubocop:disable Metrics/MethodLength
def self.data
@data ||= Hash.new do |hash, key|
@data ||= Hash.new do |hash, key| # rubocop:disable Metrics/BlockLength

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need to reenable the cop after the block

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Screenshot from 2024-06-24 16-14-27
I think its not required anymore

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the difference between an inline comment and one that is on its own.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We technically don't need to re-enable it when inline but since we've been using that enable - disable format throughout, I have modified it

Comment on lines 373 to 378
update_controller_key(vol)
if vol.is_a?(Hash)
service.volumes.new({ server: self }.merge(vol))
else
vol.server = self
vol

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you reorder the calls to:

Suggested change
update_controller_key(vol)
if vol.is_a?(Hash)
service.volumes.new({ server: self }.merge(vol))
else
vol.server = self
vol
if vol.is_a?(Hash)
vol = service.volumes.new({ server: self }.merge(vol))
end
vol.server = self
update_controller_key(vol)
vol

You will be able to remove the case statement in update_controller_key:

        def update_controller_key(vol)
            vol.controller_key ||= attributes[:scsi_controllers].first&.key || 1000
        end

because vol will always contain an object.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. Thanks Shim, updated :)

end
end
class Mock
def get_vm_first_nvme_controller(vm_id); end

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mock class returns nil, while the real method will throw an exception and never return nil.
I think it would be nice to have the behavior be equal

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are not doing that for the other(scsi and sata) controllers, I dont think it's necessary to do that as it's the mock class.

@@ -887,7 +886,6 @@ def relocation_volume_backing(volume)
end
end

# rubocop:enable Metrics/ClassLength

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should reenable the class length metric after the class definition.

@chris1984
Copy link
Collaborator

Starting to test again, @ekohl have all your concerns been addressed? If so will merge and get a release out pending all testing works again with the latest pushes.

@@ -255,7 +263,7 @@ def create_controller(controller = nil, index = 0)
operation: options[:operation],
device: controller_class.new(key: options[:key] || (1000 + index),
busNumber: options[:bus_id] || index,
**(options[:type] == RbVmomi::VIM::VirtualAHCIController ? {} : {sharedBus: controller_get_shared_from_options(options)}))
**device_options)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indenting looks off here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, it's fine I had cross checked

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks off if I look at the file, could it be your editor?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated with a new indentation. Either way, was accepted by rubocop and the general editor format

@girijaasoni girijaasoni force-pushed the nvme-controllers branch 2 times, most recently from 9b71bef to 5298015 Compare June 26, 2024 11:13
@chris1984
Copy link
Collaborator

Looks fine to me @ekohl any other thoughts before I do a final round of testing.

.rubocop.yml Outdated
@@ -42,3 +42,7 @@ Style/RescueModifier:

Metrics/MethodLength:
Max: 45

Style/MutableConstant:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be hesitant to disable this. Why is it needed?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

used the freeze method for the string constants

@chris1984
Copy link
Collaborator

@ekohl any other comments or is this good to go, tested it and it works with the latest commit and subhash did as well.

@ekohl
Copy link
Contributor

ekohl commented Jul 4, 2024

@chris1984 I still see open conversations when I look on the changed files tab. I'd expect those to either be fixed or closed with an explanation why it won't be fixed.

@girijaasoni
Copy link
Contributor Author

@chris1984 I still see open conversations when I look on the changed files tab. I'd expect those to either be fixed or closed with an explanation why it won't be fixed.

Thanks @ekohl , updated them.

@chris1984
Copy link
Collaborator

Looks fine now, just one concern:

I tested this again and I am getting Failed to create a compute VMware8 (VMware) instance ron-doria.satellite.lab.eng.rdu2.redhat.com: InvalidController: The device '1' is referring to a nonexisting controller '1,000'.

Since it keeps happening between my env and qe's I wonder if customers will start to hit this too. Is there a way we can get that controller id for all envs since you make a fix and it works on mine, but errors's on qe's vmware then you fix and it works on theirs but not mine. I am worried customer will hit that same error

Copy link
Collaborator

@chris1984 chris1984 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK, tested it on 6/7/8 and it works fine as well as on QE's VMware 8:

Screenshot 2024-07-11 at 08-34-20 vSphere - cheri-ottosen satellite lab eng rdu2 redhat com - Summary
Screenshot 2024-07-11 at 08-28-59 vSphere - annie-brigman satellite lab eng rdu2 redhat com - Summary
Screenshot 2024-07-11 at 08-40-22 vSphere - pedro-hunkele satellite lab eng rdu2 redhat com - Summary
Screenshot 2024-07-11 at 08-41-44 vSphere - teri-hargus satellite lab eng rdu2 redhat com - Summary

@chris1984 chris1984 merged commit e5eed8f into fog:master Jul 11, 2024
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants