diff --git a/app/controllers/foreman_azure_rm/concerns/hosts_controller_extensions.rb b/app/controllers/foreman_azure_rm/concerns/hosts_controller_extensions.rb index 666492f..bf294b0 100644 --- a/app/controllers/foreman_azure_rm/concerns/hosts_controller_extensions.rb +++ b/app/controllers/foreman_azure_rm/concerns/hosts_controller_extensions.rb @@ -1,11 +1,12 @@ module ForemanAzureRM module Concerns module HostsControllerExtensions + extend ActiveSupport::Concern def sizes - if (azure_rm_resource = Image.unscoped.find_by_uuid(params[:image_id])).present? - resource = azure_rm_resource.compute_resource - render :json => resource.vm_sizes(params[:region_string]).map { |size| size.name } + azure_rm_resource = ComputeResource.unscoped.find_by_id(params[:compute_resource_id]) + if azure_rm_resource.present? + render :json => azure_rm_resource.vm_sizes.map { |size| size.name } else no_sizes = _('The region you selected has no sizes associated with it') render :json => "[\"#{no_sizes}\"]" @@ -13,10 +14,9 @@ def sizes end def subnets - azure_rm_image = Image.unscoped.find_by_uuid(params[:image_id]) - if azure_rm_image.present? - azure_rm_resource = azure_rm_image.compute_resource - subnets = azure_rm_resource.subnets(params[:region]) + azure_rm_resource = ComputeResource.unscoped.find_by_id(params[:compute_resource_id]) + if azure_rm_resource.present? + subnets = azure_rm_resource.subnets if subnets.present? render :json => subnets else diff --git a/app/models/concerns/foreman_azure_rm/vm_extensions/managed_vm.rb b/app/models/concerns/foreman_azure_rm/vm_extensions/managed_vm.rb index 1e5fd4c..29ae521 100644 --- a/app/models/concerns/foreman_azure_rm/vm_extensions/managed_vm.rb +++ b/app/models/concerns/foreman_azure_rm/vm_extensions/managed_vm.rb @@ -35,7 +35,7 @@ def define_managed_storage_profile(vm_name, vhd_path, publisher, offer, sku, ver os_disk.managed_disk = managed_disk_params storage_profile.os_disk = os_disk - # Currently eliminating data disk creation since capability does not exist. + # TODO - disk creation for volume capability if vhd_path.nil? # We are using a marketplace image @@ -72,45 +72,44 @@ def define_network_profile(network_interface_card_ids) network_profile end - def create_nics(args = {}) + def create_nics(region, args = {}) nics = [] - formatted_region = args[:azure_vm][:location].gsub(/\s+/, '').downcase args[:interfaces_attributes].each do |nic, attrs| - attrs[:pubip_alloc] = attrs[:bridge] - attrs[:privip_alloc] = (attrs[:name] == 'false') ? false : true - pip_alloc = case attrs[:pubip_alloc] - when 'Static' - NetworkModels::IPAllocationMethod::Static - when 'Dynamic' - NetworkModels::IPAllocationMethod::Dynamic - when 'None' - nil - end - priv_ip_alloc = if attrs[:priv_ip_alloc] - NetworkModels::IPAllocationMethod::Static - else - NetworkModels::IPAllocationMethod::Dynamic - end - if pip_alloc.present? + private_ip = (attrs[:private_ip] == 'false') ? false : true + priv_ip_alloc = if private_ip + NetworkModels::IPAllocationMethod::Static + else + NetworkModels::IPAllocationMethod::Dynamic + end + pub_ip_alloc = case attrs[:public_ip] + when 'Static' + NetworkModels::IPAllocationMethod::Static + when 'Dynamic' + NetworkModels::IPAllocationMethod::Dynamic + when 'None' + nil + end + if pub_ip_alloc.present? public_ip_params = NetworkModels::PublicIPAddress.new.tap do |ip| - ip.location = formatted_region - ip.public_ipallocation_method = pip_alloc + ip.location = region + ip.public_ipallocation_method = pub_ip_alloc end - pip = sdk.create_or_update_pip(args[:azure_vm][:resource_group], - "#{args[:azure_vm][:vm_name]}-pip#{nic}", + + pip = sdk.create_or_update_pip(args[:resource_group], + "#{args[:vm_name]}-pip#{nic}", public_ip_params) end new_nic = sdk.create_or_update_nic( - args[:azure_vm][:resource_group], - "#{args[:azure_vm][:vm_name]}-nic#{nic}", + args[:resource_group], + "#{args[:vm_name]}-nic#{nic}", NetworkModels::NetworkInterface.new.tap do |interface| - interface.location = formatted_region + interface.location = region interface.ip_configurations = [ NetworkModels::NetworkInterfaceIPConfiguration.new.tap do |nic_conf| - nic_conf.name = "#{args[:azure_vm][:vm_name]}-nic#{nic}" + nic_conf.name = "#{args[:vm_name]}-nic#{nic}" nic_conf.private_ipallocation_method = priv_ip_alloc - nic_conf.subnet = subnets(args[:azure_vm][:location]).select{ |subnet| subnet.id == attrs[:network] }.first - nic_conf.public_ipaddress = pip.present? ? pip : nil + nic_conf.subnet = subnets.select{ |subnet| subnet.id == attrs[:network] }.first + nic_conf.public_ipaddress = pip end ] end @@ -120,7 +119,7 @@ def create_nics(args = {}) nics end - def create_managed_virtual_machine(vm_hash) + def initialize_vm(vm_hash) custom_data = vm_hash[:custom_data] msg = "Creating Virtual Machine #{vm_hash[:name]} in Resource Group #{vm_hash[:resource_group]}." logger.debug msg @@ -183,30 +182,34 @@ def create_managed_virtual_machine(vm_hash) vm.hardware_profile = ComputeModels::HardwareProfile.new.tap do |hw_profile| hw_profile.vm_size = vm_hash[:vm_size] end - vm.network_profile = define_network_profile(vm_hash[:network_interface_card_ids]) end - response = sdk.create_or_update_vm(vm_hash[:resource_group], vm_hash[:name], vm_create_params) - logger.debug "Virtual Machine #{vm_hash[:name]} Created Successfully." - response + vm_create_params + end + + def create_managed_virtual_machine(vm_hash) + vm_params = initialize_vm(vm_hash) + vm_params.network_profile = define_network_profile(vm_hash[:network_interface_card_ids]) + sdk.create_or_update_vm(vm_hash[:resource_group], vm_hash[:name], vm_params) end - def create_vm_extension(args = {}) - if args[:azure_vm][:script_command].present? || args[:azure_vm][:script_uris].present? + def create_vm_extension(region, args = {}) + if args[:script_command].present? || args[:script_uris].present? + args[:script_uris] ||= args[:script_uris].to_s extension = ComputeModels::VirtualMachineExtension.new - if args[:azure_vm][:platform] == 'Linux' + if args[:platform] == 'Linux' extension.publisher = 'Microsoft.Azure.Extensions' extension.virtual_machine_extension_type = 'CustomScript' extension.type_handler_version = '2.0' end extension.auto_upgrade_minor_version = true - extension.location = args[:azure_vm][:location].gsub(/\s+/, '').downcase + extension.location = region extension.settings = { - 'commandToExecute' => args[:azure_vm][:script_command], - 'fileUris' => args[:azure_vm][:script_uris].split(',') + 'commandToExecute' => args[:script_command], + 'fileUris' => args[:script_uris].split(',') } - sdk.create_or_update_vm_extensions(args[:azure_vm][:resource_group], - args[:azure_vm][:vm_name], + sdk.create_or_update_vm_extensions(args[:resource_group], + args[:vm_name], 'ForemanCustomScript', extension) end diff --git a/app/models/foreman_azure_rm/azure_rm.rb b/app/models/foreman_azure_rm/azure_rm.rb index 036b5cc..162a2e2 100644 --- a/app/models/foreman_azure_rm/azure_rm.rb +++ b/app/models/foreman_azure_rm/azure_rm.rb @@ -1,6 +1,6 @@ # This Model contains code modified as per azure-sdk # and removed dependencies from fog-azure-rm. -# + require 'base64' module ForemanAzureRM @@ -9,13 +9,10 @@ class AzureRM < ComputeResource include VMExtensions::ManagedVM alias_attribute :sub_id, :user alias_attribute :secret_key, :password - alias_attribute :app_ident, :url + alias_attribute :region, :url alias_attribute :tenant, :uuid - validates :user, :presence => true - validates :password, :presence => true - validates :url, :presence => true - validates :uuid, :presence => true + validates :user, :password, :url, :uuid, :app_ident, :presence => true has_one :key_pair, :foreign_key => :compute_resource_id, :dependent => :destroy @@ -23,6 +20,7 @@ class AzureRM < ComputeResource class VMContainer attr_accessor :virtualmachines + delegate :each, to: :virtualmachines def initialize @virtualmachines = [] @@ -33,6 +31,14 @@ def all(_options = {}) end end + def app_ident + attrs[:app_ident] + end + + def app_ident=(name) + attrs[:app_ident] = name + end + def sdk @sdk ||= ForemanAzureRM::AzureSDKAdapter.new(tenant, app_ident, secret_key, sub_id) end @@ -45,7 +51,7 @@ def self.model_name ComputeResource.model_name end - def provider_friendly_name + def self.provider_friendly_name 'Azure Resource Manager' end @@ -55,33 +61,18 @@ def capabilities def regions [ - 'West Europe', - 'Central US', - 'South Central US', - 'North Central US', - 'West Central US', - 'East US', - 'East US 2', - 'West US', - 'West US 2' + ['West Europe', 'westeurope'], + ['Central US', 'centralus'], + ['South Central US', 'southcentralus'], + ['North Central US', 'northcentralus'], + ['West Central US', 'westcentralus'], + ['East US', 'eastus'], + ['East US 2', 'eastus2'], + ['West US', 'westus'], + ['West US 2', 'westus2'] ] end - def storage_accts(region = nil) - stripped_region = region.gsub(/\s+/, '').downcase - acct_names = [] - if region.nil? - sdk.get_storage_accts.each do |acct| - acct_names << acct.name - end - else - (sdk.get_storage_accts.select { |acct| acct.region == stripped_region }).each do |acct| - acct_names << acct.name - end - end - acct_names - end - def resource_groups sdk.rgs end @@ -93,20 +84,39 @@ def test_connection(options = {}) super(options) end - def new_vm(attr = {}) - AzureRMCompute.new(sdk: sdk) + def new_vm(args = {}) + return AzureRMCompute.new(sdk: sdk) if args.empty? || args[:image_id].nil? + opts = vm_instance_defaults.merge(args.to_h).deep_symbolize_keys + # convert rails nested_attributes into a plain hash + nested_args = opts.delete(:interfaces_attributes) + opts[:interfaces] = nested_attributes_for(:interfaces, nested_args) if nested_args + + opts.reject! { |k, v| v.nil? } + + raw_vm = initialize_vm(location: region, + resource_group: opts[:resource_group], + vm_size: opts[:vm_size], + username: opts[:username], + password: opts[:password], + platform: opts[:platform], + ssh_key_data: opts[:ssh_key_data], + os_disk_caching: opts[:os_disk_caching], + vhd_path: opts[:image_id], + premium_os_disk: opts[:premium_os_disk] + ) + if opts[:interfaces].present? + ifaces = [] + opts[:interfaces].each_with_index do |iface_attrs, i| + ifaces << new_interface(iface_attrs) + end + end + AzureRMCompute.new(azure_vm: raw_vm ,sdk: sdk, resource_group: opts[:resource_group], nics: ifaces) end def provided_attributes super.merge({ :ip => :provisioning_ip_address }) end - def host_interfaces_attrs(host) - host.interfaces.select(&:physical?).each.with_index.reduce({}) do |hash, (nic, index)| - hash.merge(index.to_s => nic.compute_attributes.merge(ip: nic.ip, ip6: nic.ip6)) - end - end - def available_vnets(attr = {}) virtual_networks end @@ -115,22 +125,12 @@ def available_networks(attr = {}) subnets end - def available_subnets - subnets - end - - def virtual_networks(region = nil) - if region.nil? - sdk.vnets - else - stripped_region = region.gsub(/\s+/, '').downcase - sdk.vnets.select { |vnet| vnet.location == stripped_region } - end + def virtual_networks + @virtual_networks ||= sdk.vnets.select { |vnet| vnet.location == region } end - def subnets(region = nil) - stripped_region = region.gsub(/\s+/, '').downcase - vnets = virtual_networks(stripped_region) + def subnets + vnets = virtual_networks subnets = [] vnets.each do |vnet| subnets.concat(sdk.subnets(vnet.resource_group, vnet.name)) @@ -138,14 +138,18 @@ def subnets(region = nil) subnets end - def new_interface(attr = {}) - # WIP - # calls nic_cards method in adapter - # causes compute profiles issue - # NetworkModels::NetworkInterface.new + alias_method :available_subnets, :subnets + + def new_interface(attrs = {}) + args = { :network => "", :public_ip => "", :private_ip => false, 'persisted?' => false }.merge(attrs.to_h) + OpenStruct.new(args) end - def vm_sizes(region) + def editable_network_interfaces? + true + end + + def vm_sizes sdk.list_vm_sizes(region) end @@ -154,16 +158,26 @@ def associated_host(vm) end def vm_instance_defaults - ActiveSupport::HashWithIndifferentAccess.new + super.deep_merge( + interfaces: [new_interface] + ) end - def vms + def vm_nics(vm) + ifaces = [] + vm.network_profile.network_interfaces.each do |nic| + nic_rg = (split_nic_id = nic.id.split('/'))[4] + nic_name = split_nic_id[-1] + ifaces << sdk.vm_nic(nic_rg, nic_name) + end + ifaces + end + + def vms(attrs = {}) container = VMContainer.new - # Load all vms - resource_groups.each do |rg| - sdk.list_vms(rg).each do |vm| - container.virtualmachines << AzureRMCompute.new(azure_vm: vm, sdk:sdk) - end + # Load all vms of the region + sdk.list_vms(region).each do |vm| + container.virtualmachines << AzureRMCompute.new(azure_vm: vm, sdk:sdk, nics: vm_nics(vm)) end container end @@ -187,44 +201,46 @@ def user_data_supported? end def create_vm(args = {}) - args[:azure_vm][:vm_name] = args[:name].split('.')[0] - nics = create_nics(args) - if args[:azure_vm][:password].present? && !args[:azure_vm][:ssh_key_data].present? - sudoers_cmd = "$echo #{args[:azure_vm][:password]} | sudo -S echo '\"#{args[:azure_vm][:username]}\" ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/waagent" - if args[:azure_vm][:script_command].present? - # to run the script_cmd given through form - # as username - args[:azure_vm][:script_command] = sudoers_cmd + " ; su - \"#{args[:azure_vm][:username]}\" -c \"#{args[:azure_vm][:script_command]}\"" + args = args.to_h.deep_symbolize_keys + args[:vm_name] = args[:name].split('.')[0] + nics = create_nics(region, args) + if args[:password].present? && !args[:ssh_key_data].present? + sudoers_cmd = "$echo #{args[:password]} | sudo -S echo '\"#{args[:username]}\" ALL=(ALL) NOPASSWD: ALL' > /etc/sudoers.d/waagent" + if args[:script_command].present? + # to run the script_cmd given through form as username + args[:script_command] = sudoers_cmd + " ; su - \"#{args[:username]}\" -c \"#{args[:script_command]}\"" else - args[:azure_vm][:script_command] = sudoers_cmd + args[:script_command] = sudoers_cmd end disable_password_auth = false - elsif args[:azure_vm][:ssh_key_data].present? && !args[:azure_vm][:password].present? + elsif args[:ssh_key_data].present? && !args[:password].present? disable_password_auth = true else disable_password_auth = false end + vm = create_managed_virtual_machine( - name: args[:azure_vm][:vm_name], - location: args[:azure_vm][:location], - resource_group: args[:azure_vm][:resource_group], - vm_size: args[:azure_vm][:vm_size], - username: args[:azure_vm][:username], - password: args[:azure_vm][:password], - ssh_key_data: args[:azure_vm][:ssh_key_data], + name: args[:vm_name], + location: region, + resource_group: args[:resource_group], + vm_size: args[:vm_size], + username: args[:username], + password: args[:password], + ssh_key_data: args[:ssh_key_data], disable_password_authentication: disable_password_auth, network_interface_card_ids: nics.map(&:id), - platform: args[:azure_vm][:platform], + platform: args[:platform], vhd_path: args[:image_id], - os_disk_caching: args[:azure_vm][:os_disk_caching], - premium_os_disk: args[:azure_vm][:premium_os_disk], + os_disk_caching: args[:os_disk_caching], + premium_os_disk: args[:premium_os_disk], custom_data: args[:user_data], - script_command: args[:azure_vm][:script_command], - script_uris: args[:azure_vm][:script_uris], + script_command: args[:script_command], + script_uris: args[:script_uris], ) - create_vm_extension(args) + logger.debug "Virtual Machine #{args[:vm_name]} Created Successfully." + create_vm_extension(region, args) # return the vm object using azure_vm - return_vm = AzureRMCompute.new(azure_vm: vm, sdk: sdk) + AzureRMCompute.new(azure_vm: vm, sdk: sdk, resource_group: args[:resource_group], nics: vm_nics(vm)) rescue RuntimeError => e Foreman::Logging.exception('Unhandled Azure RM error', e) destroy_vm vm.id if vm @@ -232,15 +248,13 @@ def create_vm(args = {}) end def destroy_vm(uuid) - #vm.azure_vm because that's the azure object and vm is the wrapper vm = find_vm_by_uuid(uuid) - vm_name = vm.name - rg_name = vm.azure_vm.resource_group + rg_name = vm.resource_group os_disk = vm.azure_vm.storage_profile.os_disk data_disks = vm.azure_vm.storage_profile.data_disks nic_ids = vm.network_interface_card_ids - sdk.delete_vm(rg_name, vm_name) + sdk.delete_vm(rg_name, vm.name) nic_ids.each do |nic_id| nic = sdk.vm_nic(rg_name, nic_id.split('/')[-1]) diff --git a/app/models/foreman_azure_rm/azure_rm_compute.rb b/app/models/foreman_azure_rm/azure_rm_compute.rb index 4048475..ded7f05 100644 --- a/app/models/foreman_azure_rm/azure_rm_compute.rb +++ b/app/models/foreman_azure_rm/azure_rm_compute.rb @@ -2,12 +2,29 @@ module ForemanAzureRM class AzureRMCompute attr_accessor :sdk attr_accessor :azure_vm + attr_accessor :resource_group + attr_accessor :nics + attr_accessor :image_id delegate :name, to: :azure_vm, allow_nil: true - def initialize(azure_vm: ComputeModels::VirtualMachine.new, sdk: sdk) + def initialize(azure_vm: ComputeModels::VirtualMachine.new, + sdk: sdk, + resource_group: azure_vm.resource_group, + nics: []) + @azure_vm = azure_vm @sdk = sdk + @resource_group ||= resource_group + @nics ||= nics + @azure_vm.hardware_profile ||= ComputeModels::HardwareProfile.new + @azure_vm.os_profile ||= ComputeModels::OSProfile.new + @azure_vm.os_profile.linux_configuration ||= ComputeModels::LinuxConfiguration.new + @azure_vm.os_profile.linux_configuration.ssh ||= ComputeModels::SshConfiguration.new + @azure_vm.os_profile.linux_configuration.ssh.public_keys ||= [ComputeModels::SshPublicKey.new] + @azure_vm.storage_profile ||= ComputeModels::StorageProfile.new + @azure_vm.storage_profile.os_disk ||= ComputeModels::OSDisk.new + @azure_vm.storage_profile.os_disk.managed_disk ||= ComputeModels::ManagedDiskParameters.new end def id @@ -18,10 +35,6 @@ def persisted? !!identity && !!id end - def vm_size - @azure_vm.hardware_profile.vm_size - end - def wait_for(_timeout = 0, _interval = 0, &block) instance_eval(&block) return true @@ -37,10 +50,12 @@ def state def start sdk.start_vm(@azure_vm.resource_group, name) + true end def stop sdk.stop_vm(@azure_vm.resource_group, name) + true end def vm_status @@ -48,41 +63,45 @@ def vm_status end def network_interface_card_ids + return nil unless @azure_vm.network_profile nics = @azure_vm.network_profile.network_interfaces nics.map(&:id) end def provisioning_ip_address + public_ip_address || private_ip_address + end + + def public_ip_address interfaces.each do |nic| nic.ip_configurations.each do |configuration| next unless configuration.primary - if configuration.public_ipaddress.present? - ip_id = configuration.public_ipaddress.id - ip_rg = ip_id.split('/')[4] - ip_name = ip_id.split('/')[-1] - public_ip = sdk.public_ip(ip_rg, ip_name) - return public_ip.ip_address - else - return configuration.private_ipaddress - end + return nil if configuration.public_ipaddress.blank? + ip_id = configuration.public_ipaddress.id + ip_rg = ip_id.split('/')[4] + ip_name = ip_id.split('/')[-1] + public_ip = sdk.public_ip(ip_rg, ip_name) + return public_ip.ip_address end - end + end end - def interfaces - interfaces = [] - unless network_interface_card_ids.nil? - network_interface_card_ids.each do |nic_id| - nic_rg = nic_id.split('/')[4] - nic_name = nic_id.split('/')[-1] - interfaces << sdk.vm_nic(nic_rg, nic_name) + def private_ip_address + interfaces.each do |nic| + nic.ip_configurations.each do |configuration| + next unless configuration.primary + if configuration.private_ipaddress.present? + return private_ip_address = configuration.private_ipaddress + end end end - interfaces end - def interfaces=(setifaces) - @azure_vm.network_profile.network_interfaces = setifaces + def interfaces + nics + end + + def interfaces_attributes=(attrs) end def ip_addresses @@ -97,7 +116,45 @@ def identity=(setuuid) @azure_vm.name = setuuid end - def image_id + # Following properties are for AzureRM + # These are not part of Foreman's interface + + def vm_size + @azure_vm.hardware_profile.vm_size + end + + def platform + @azure_vm.storage_profile.os_disk.os_type + end + + def username + @azure_vm.os_profile.admin_username + end + + def password + @azure_vm.os_profile.admin_password + end + + def ssh_key_data + # since you can only give one additional + # sshkey through foreman's UI + sshkey = @azure_vm.os_profile.linux_configuration.ssh.public_keys[1] + return unless sshkey.present? + sshkey.key_data + end + + def premium_os_disk + @azure_vm.storage_profile.os_disk.managed_disk.storage_account_type + end + + def os_disk_caching + @azure_vm.storage_profile.os_disk.caching + end + + def script_command + end + + def script_uris end end end diff --git a/app/views/api/v2/compute_resources/azure_rm.json.rabl b/app/views/api/v2/compute_resources/azure_rm.json.rabl deleted file mode 100644 index e8c1c0f..0000000 --- a/app/views/api/v2/compute_resources/azure_rm.json.rabl +++ /dev/null @@ -1 +0,0 @@ -attributes :user, :uuid diff --git a/app/views/api/v2/compute_resources/azurerm.json.rabl b/app/views/api/v2/compute_resources/azurerm.json.rabl new file mode 100644 index 0000000..b3e9ef0 --- /dev/null +++ b/app/views/api/v2/compute_resources/azurerm.json.rabl @@ -0,0 +1 @@ +attributes :tenant, :app_ident, :sub_id, :secret_key, :region diff --git a/app/views/compute_resources/form/_azurerm.html.erb b/app/views/compute_resources/form/_azurerm.html.erb index 18b4fdb..90e38a9 100644 --- a/app/views/compute_resources/form/_azurerm.html.erb +++ b/app/views/compute_resources/form/_azurerm.html.erb @@ -1,8 +1,10 @@ -<%= text_f f, :url, :label => _('Client ID'), :required => true %> +<%= text_f f, :app_ident, :label => _('Client ID'), :required => true %> <%= password_f f, :password, :label => _('Client Secret'), :keep_value => true, :required => true %> <%= text_f f, :user, :label => _('Subscription ID'), :required => true %> <%= text_f f, :uuid, :label => _('Tenant ID'), :required => true %> +<%= selectable_f(f, :url, f.object.regions, {}, {:label => _('Azure Region'), :required => true }) %> +