module Chef::Knife::AzurermBase

Public Class Methods

included(includer) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 31
def self.included(includer)
  includer.class_eval do
    deps do
      require "readline"
      require "chef/json_compat"
      require_relative "../../../azure/resource_management/ARM_interface"
      require "chef/mixin/shell_out"
      require "time" unless defined?(Time)
      require "json" unless defined?(JSON)

      if Chef::Platform.windows?
        require_relative "../../azure/resource_management/windows_credentials"
        include Azure::ARM::WindowsCredentials
      end
    end

    option :azure_resource_group_name,
      short: "-r RESOURCE_GROUP_NAME",
      long: "--azure-resource-group-name RESOURCE_GROUP_NAME",
      description: "The Resource Group name."
  end
end

Public Instance Methods

authentication_details() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 87
def authentication_details
  if is_azure_cred?
    return { azure_tenant_id: config[:azure_tenant_id], azure_client_id: config[:azure_client_id], azure_client_secret: config[:azure_client_secret] }
  elsif Chef::Platform.windows?
    token_details = token_details_for_windows
  else
    token_details = token_details_for_linux
  end

  check_token_validity(token_details)
end
azure_authentication() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 144
def azure_authentication
  ui.log("Authenticating...")
  Mixlib::ShellOut.new("#{@azure_prefix} vm show 'knifetest@resourcegroup' testvm", timeout: 30).run_command
rescue Mixlib::ShellOut::CommandTimeout
rescue Exception
  raise_azure_status
end
check_token_validity(token_details) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 152
def check_token_validity(token_details)
  unless is_token_valid?(token_details)
    token_details = refresh_token
    raise_azure_status unless is_token_valid?(token_details)
  end
  token_details
end
find_file(name) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 201
def find_file(name)
  name = ::File.expand_path(name)
  config_dir = Chef::Knife.chef_config_dir
  if File.exist? name
    file = name
  elsif config_dir && File.exist?(File.join(config_dir, name))
    file = File.join(config_dir, name)
  elsif File.exist?(File.join(ENV["HOME"], ".chef", name))
    file = File.join(ENV["HOME"], ".chef", name)
  else
    ui.error("Unable to find file - " + name)
    exit 1
  end
  file
end
get_azure_cli_version() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 99
def get_azure_cli_version
  if @azure_version != ""
    get_version = shell_out!("azure -v || az -v | grep azure-cli", returns: [0]).stdout
    @azure_version = get_version.gsub(/[^0-9.]/, "")
  end
  @azure_prefix = @azure_version.to_i < 2 ? "azure" : "az"
  @azure_version
end
is_token_valid?(token_details) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 127
def is_token_valid?(token_details)
  time_difference = Time.parse(token_details[:expiry_time]) - Time.now.utc
  if time_difference <= 0
    false
  elsif time_difference <= 600 # 600sec = 10min
    # This is required otherwise a long running command may fail inbetween if the token gets expired.
    raise "Token will expire within 10 minutes. Please run '#{@azure_prefix} login' command"
  else
    true
  end
end
msg_server_summary(server) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 217
def msg_server_summary(server)
  puts "\n\n"
  if server.provisioningstate == "Succeeded"
    Chef::Log.info("Server creation went successful.")
    puts "\nServer Details are:\n"

    msg_pair("Server ID", server.id)
    msg_pair("Server Name", server.name)
    msg_pair("Server Public IP Address", server.publicipaddress)
    if is_image_windows?
      msg_pair("Server RDP Port", server.rdpport)
    else
      msg_pair("Server SSH Port", server.sshport)
    end
    msg_pair("Server Location", server.locationname)
    msg_pair("Server OS Type", server.ostype)
    msg_pair("Server Provisioning State", server.provisioningstate)
  else
    Chef::Log.info("Server Creation Failed.")
  end

  puts "\n\n"

  if server.resources.provisioning_state == "Succeeded"
    Chef::Log.info("Server Extension creation went successful.")
    puts "\nServer Extension Details are:\n"

    msg_pair("Server Extension ID", server.resources.id)
    msg_pair("Server Extension Name", server.resources.name)
    msg_pair("Server Extension Publisher", server.resources.publisher)
    msg_pair("Server Extension Type", server.resources.type)
    msg_pair("Server Extension Type Handler Version", server.resources.type_handler_version)
    msg_pair("Server Extension Provisioning State", server.resources.provisioning_state)
  else
    Chef::Log.info("Server Extension Creation Failed.")
  end
  puts "\n"
end
parse_publish_settings_file(filename) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 174
def parse_publish_settings_file(filename)
  require "nokogiri" unless defined?(Nokogiri)
  require "base64" unless defined?(Base64)
  require "openssl" unless defined?(OpenSSL)
  require "uri" unless defined?(URI)
  begin
    doc = Nokogiri::XML(File.open(find_file(filename)))
    profile = doc.at_css("PublishProfile")
    subscription = profile.at_css("Subscription")
    # check given PublishSettings XML file format.Currently PublishSettings file have two different XML format
    if profile.attribute("SchemaVersion").nil?
      management_cert = OpenSSL::PKCS12.new(Base64.decode64(profile.attribute("ManagementCertificate").value))
      config[:azure_api_host_name] = URI(profile.attribute("Url").value).host
    elsif profile.attribute("SchemaVersion").value == "2.0"
      management_cert = OpenSSL::PKCS12.new(Base64.decode64(subscription.attribute("ManagementCertificate").value))
      config[:azure_api_host_name] = URI(subscription.attribute("ServiceManagementUrl").value).host
    else
      ui.error("Publish settings file Schema not supported - " + filename)
    end
    config[:azure_mgmt_cert] = management_cert.certificate.to_pem + management_cert.key.to_pem
    config[:azure_subscription_id] = doc.at_css("Subscription").attribute("Id").value
  rescue => error
    puts "#{error.class} and #{error.message}"
    exit 1
  end
end
refresh_token() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 139
def refresh_token
  azure_authentication
  token_details = Chef::Platform.windows? ? token_details_for_windows : token_details_for_linux
end
service() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 54
def service
  details = authentication_details
  details.update(azure_subscription_id: config[:azure_subscription_id])
  @service ||= begin
                require_relative "../../../azure/resource_management/ARM_interface"
                service = Azure::ResourceManagement::ARMInterface.new(details)
              end
  @service.ui = ui
  @service
end
token_details_for_linux() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 116
def token_details_for_linux
  token_details_from_accessToken_file
end
token_details_for_windows() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 108
def token_details_for_windows
  if is_old_xplat?
    token_details_from_WCM
  else
    is_WCM_env_var_set? ? token_details_from_WCM : token_details_from_accessToken_file
  end
end
token_details_from_accessToken_file() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 120
def token_details_from_accessToken_file
  home_dir = File.expand_path("~")
  file = File.read(home_dir + "/.azure/accessTokens.json")
  file = JSON.parse(file)
  { tokentype: file[-1]["tokenType"], user: file[-1]["userId"], token: file[-1]["accessToken"], clientid: file[-1]["_clientId"], expiry_time: file[-1]["expiresOn"], refreshtoken: file[-1]["refreshToken"] }
end
validate_arm_keys!(*keys) click to toggle source

validates ARM mandatory keys

# File lib/chef/knife/helpers/azurerm_base.rb, line 66
def validate_arm_keys!(*keys)
  parse_publish_settings_file(config[:azure_publish_settings_file]) unless config[:azure_publish_settings_file].nil?
  keys.push(:azure_subscription_id)

  if azure_cred?
    validate_azure_login
  else
    keys.concat(%i{azure_tenant_id azure_client_id azure_client_secret})
  end

  errors = []
  keys.each do |k|
    if config[k].nil?
      errors << "You did not provide a valid '#{pretty_key(k)}' value. Please set knife[:#{k}] in your config.rb (knife.rb)."
    end
  end
  if errors.each { |e| ui.error(e) }.any?
    exit 1
  end
end
validate_azure_login() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 160
def validate_azure_login
  if Chef::Platform.windows? && (is_old_xplat? || is_WCM_env_var_set?)
    # cmdkey command is used for accessing windows credential manager
    xplat_creds_cmd = Mixlib::ShellOut.new("cmdkey /list | findstr AzureXplatCli")
    result = xplat_creds_cmd.run_command
    raise login_message if result.stdout.nil? || result.stdout.empty?
  else
    home_dir = File.expand_path("~")
    if !File.exist?(home_dir + "/.azure/accessTokens.json") || ( File.size?(home_dir + "/.azure/accessTokens.json") <= 2 )
      raise login_message
    end
  end
end
validate_params!() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 256
def validate_params!
  if config[:connection_user].nil?
    raise ArgumentError, "Please provide --connection-user option for authentication."
  end

  unless config[:connection_password].nil? ^ config[:ssh_public_key].nil?
    raise ArgumentError, "Please specify either --connection-password or --ssh-public-key option for authentication."
  end

  if config[:azure_vnet_subnet_name] && !config[:azure_vnet_name]
    raise ArgumentError, "When --azure-vnet-subnet-name is specified, the --azure-vnet-name must also be specified."
  end

  if config[:azure_vnet_subnet_name] == "GatewaySubnet"
    raise ArgumentError, "GatewaySubnet cannot be used as the name for --azure-vnet-subnet-name option. GatewaySubnet can only be used for virtual network gateways."
  end

  if config[:node_ssl_verify_mode] && !%w{none peer}.include?(config[:node_ssl_verify_mode])
    raise ArgumentError, "Invalid value '#{config[:node_ssl_verify_mode]}' for --node-ssl-verify-mode. Use Valid values i.e 'none', 'peer'."
  end

  if !is_image_windows?
    if (config[:azure_vm_name].match(/^(?=.*[a-zA-Z-])([a-zA-z0-9-]{1,64})$/)).nil?
      raise ArgumentError, "VM name can only contain alphanumeric and hyphen(-) characters and maximum length cannot exceed 64 characters."
    end
  elsif (config[:azure_vm_name].match(/^(?=.*[a-zA-Z-])([a-zA-z0-9-]{1,15})$/)).nil?
    raise ArgumentError, "VM name can only contain alphanumeric and hyphen(-) characters and maximum length cannot exceed 15 characters."
  end

  if config[:server_count].to_i > 5
    raise ArgumentError, "Maximum allowed value of --server-count is 5."
  end

  if config[:daemon]
    unless is_image_windows?
      raise ArgumentError, "The daemon option is only support for Windows nodes."
    end

    unless %w{none service task}.include?(config[:daemon])
      raise ArgumentError, "Invalid value for --daemon option. Use valid daemon values i.e 'none', 'service' and 'task'."
    end
  end

  if config[:azure_image_os_type]
    unless %w{ubuntu centos rhel debian windows}.include?(config[:azure_image_os_type])
      raise ArgumentError, "Invalid value of --azure-image-os-type. Accepted values ubuntu|centos|rhel|debian|windows"
    end
  end

  config[:ohai_hints] = format_ohai_hints(config[:ohai_hints])
  validate_ohai_hints unless config[:ohai_hints].casecmp("default").zero?
end

Private Instance Methods

azure_cred?() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 329
def azure_cred?
  config[:azure_tenant_id].nil? || config[:azure_client_id].nil? || config[:azure_client_secret].nil?
end
is_WCM_env_var_set?() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 339
def is_WCM_env_var_set?
  ENV["AZURE_USE_SECURE_TOKEN_STORAGE"].nil? ? false : true
end
is_azure_cred?() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 325
def is_azure_cred?
  config[:azure_tenant_id] && config[:azure_client_id] && config[:azure_client_secret]
end
is_image_windows?() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 321
def is_image_windows?
  config[:azure_image_reference_offer] =~ /WindowsServer.*/
end
is_old_xplat?() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 333
def is_old_xplat?
  return true unless @azure_version

  Gem::Version.new(@azure_version) < Gem::Version.new(XPLAT_VERSION_WITH_WCM_DEPRECATED)
end
login_message() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 347
def login_message
  ## Older versions of the Azure CLI on Windows stored credentials in a unique way
  ## in Windows Credentails Manager (WCM).
  ## Newer versions use the same pattern across platforms where credentials gets
  ## stored in ~/.azure/accessTokens.json file.
  "Please run XPLAT's '#{@azure_prefix} login' command OR specify azure_tenant_id, azure_subscription_id, azure_client_id, azure_client_secret in your config.rb (knife.rb)."
end
msg_pair(label, value, color = :cyan) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 311
def msg_pair(label, value, color = :cyan)
  if value && !value.to_s.empty?
    puts "#{ui.color(label, color)}: #{value}"
  end
end
pretty_key(key) click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 317
def pretty_key(key)
  key.to_s.tr("_", " ").gsub(/\w+/) { |w| (w =~ /(ssh)|(aws)/i) ? w.upcase : w.capitalize }
end
raise_azure_status() click to toggle source
# File lib/chef/knife/helpers/azurerm_base.rb, line 343
def raise_azure_status
  raise "Token has expired. Please run '#{@azure_prefix} login' command"
end