#
# Author:: Seth Chisamore (<schisamo@chef.io>)
# Author:: Matt Ray (<matt@chef.io>)
# Author:: Chirag Jog (<chirag@clogeny.com>)
# Author:: Lance Albertson (<lance@osuosl.org>)
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "chef/knife/cloud/server/create_command"
require_relative "openstack_helpers"
require_relative "cloud/openstack_server_create_options"
require_relative "cloud/openstack_service"
require_relative "cloud/openstack_service_options"
require "chef/knife/cloud/exceptions"

class Chef
  class Knife
    class Cloud
      class OpenstackServerCreate < ServerCreateCommand
        include OpenstackHelpers
        include OpenstackServerCreateOptions
        include OpenstackServiceOptions

        banner "knife openstack server create (options)"

        def before_exec_command
          super
          # setup the create options
          @create_options = {
            server_def: {
              # servers require a name, knife-cloud generates the chef_node_name
              :name => config[:chef_node_name],
              :image_ref => service.get_image(config[:image]).id,
              :flavor_ref => service.get_flavor(config[:flavor]).id,
              :security_groups => config[:openstack_security_groups],
              :availability_zone => config[:availability_zone],
              "os:scheduler_hints" => config[:openstack_scheduler_hints],
              :metadata => config[:metadata],
              :key_name => config[:openstack_ssh_key_id],
            },
            server_create_timeout: config[:server_create_timeout],
          }
          unless config[:openstack_volumes].nil?
            counter = 99
            @create_options[:server_def][:block_device_mapping] = config[:openstack_volumes].map do |vol|
              counter += 1
              {
                volume_id: vol,
                delete_on_termination: false,
                device_name: "/dev/vd" + counter.chr,
                volume_size: nil,
              }
            end
          end

          @create_options[:server_def][:user_data] = config[:user_data] if config[:user_data]
          @create_options[:server_def][:nics] = config[:network_ids].map { |nic| nic_id = { "net_id" => nic } } if config[:network_ids]

          Chef::Log.debug("Create server params - server_def = #{@create_options[:server_def]}")
          # set columns_with_info map
          @columns_with_info = [
            { label: "Instance ID", key: "id" },
            { label: "Name", key: "name" },
            { label: "Public IP", key: "addresses", value_callback: method(:primary_public_ip_address) },
            { label: "Private IP", key: "addresses", value_callback: method(:primary_private_ip_address) },
            { label: "Flavor", key: "flavor", value_callback: method(:get_id) },
            { label: "Image", key: "image", value_callback: method(:get_id) },
            { label: "Keypair", key: "key_name" },
            { label: "State", key: "state" },
            { label: "Availability Zone", key: "availability_zone" },
          ]
        end

        def get_id(value)
          value["id"]
        end

        # Setup the floating ip after server creation.
        def after_exec_command
          Chef::Log.debug("Addresses #{server.addresses}")
          msg_pair("Public IP Address", primary_public_ip_address(server.addresses)) if primary_public_ip_address(server.addresses)
          msg_pair("Private IP Address", primary_private_ip_address(server.addresses)) if primary_private_ip_address(server.addresses)

          floating_address = config[:openstack_floating_ip]
          bind_ip = primary_network_ip_address(server.addresses, server.addresses.keys[0])
          Chef::Log.debug("Floating IP Address requested #{floating_address}")
          unless floating_address == "-1" # no floating IP requested
            addresses = service.connection.addresses
            # floating requested without value
            if floating_address.nil?
              free_floating = addresses.find_index { |a| a.fixed_ip.nil? }
              begin
                if free_floating.nil? # no free floating IP found
                  error_message = "Unable to assign a Floating IP from allocated IPs."
                  ui.fatal(error_message)
                  raise CloudExceptions::ServerSetupError, error_message
                else
                  floating_address = addresses[free_floating].ip
                end
              rescue CloudExceptions::ServerSetupError => e
                cleanup_on_failure
                raise e
              end
            end

            # Pull the port_id for the associate_floating_ip
            port_id = @service.network.list_ports[:body]["ports"].find { |x| x["fixed_ips"][0]["ip_address"] == bind_ip }["id"]
            fixed_ip_address = service.network.list_ports[:body]["ports"].find { |x| x["id"] == port_id }["fixed_ips"][0]["ip_address"]

            floating_ip_id = get_floating_ip_id(floating_address)
            # Associate the floating ip via the neutron/network api
            @service.network.associate_floating_ip(floating_ip_id, port_id, fixed_ip_address: fixed_ip_address)

            # a bit of a hack, but server.reload takes a long time
            (server.addresses["public"] ||= []) << { "version" => 4, "addr" => floating_address }
            msg_pair("Floating IP Address", floating_address)
          end

          Chef::Log.debug("Addresses #{server.addresses}")
          Chef::Log.debug("Public IP Address actual: #{primary_public_ip_address(server.addresses)}") if primary_public_ip_address(server.addresses)

          msg_pair("Private IP Address", primary_private_ip_address(server.addresses)) if primary_private_ip_address(server.addresses)
          super
        end

        def before_bootstrap
          super

          # Use SSH password either specified from command line or from openstack server instance
          config[:ssh_password] = config[:ssh_password] || server.password unless config[:openstack_ssh_key_id]

          # The bootstrap network is always initialized to 'public' when a network name isn't specified.  Therefore,
          # only set the bootstrap network to 'private' if still initialized to public and nothing was specified for
          # the private network name.
          config[:bootstrap_network] = "private" if config[:private_network] && config[:bootstrap_network] == "public"

          # Which IP address to bootstrap
          unless config[:network] # --no-network
            bootstrap_ip_address = primary_public_ip_address(server.addresses) ||
              primary_private_ip_address(server.addresses) ||
              server.addresses.first[1][0]["addr"]
            Chef::Log.debug("No Bootstrap Network: #{config[:bootstrap_network]}")
          else
            bootstrap_ip_address = primary_network_ip_address(server.addresses, config[:bootstrap_network])
            Chef::Log.debug("Bootstrap Network: #{config[:bootstrap_network]}")
          end

          Chef::Log.debug("Bootstrap IP Address: #{bootstrap_ip_address}")
          if bootstrap_ip_address.nil?
            error_message = "No IP address available for bootstrapping."
            ui.error(error_message)
            raise CloudExceptions::BootstrapError, error_message
          end
          config[:bootstrap_ip_address] = bootstrap_ip_address

          config[:hints] ||= {}
          config[:hints]["openstack"] ||= {}
        end

        def validate_params!
          # set param vm_name to a random value if the name is not set by the user (plugin)
          config[:chef_node_name] = get_node_name(config[:chef_node_name], config[:chef_node_name_prefix])

          errors = []

          if config[:connection_protocol] == "winrm"
            if config[:connection_password].nil?
              errors << "You must provide Connection Password."
            end
          elsif config[:connection_protocol] != "ssh"
            errors << "You must provide a valid bootstrap protocol. options [ssh/winrm]. For linux type images, options [ssh]"
          end

          errors << "You must provide --image-os-type option [windows/linux]" unless %w{windows linux}.include?(config[:image_os_type])
          error_message = ""
          raise CloudExceptions::ValidationError, error_message if errors.each { |e| ui.error(e); error_message = "#{error_message} #{e}." }.any?
        end

        def is_image_valid?
          service.get_image(config[:image]).nil? ? false : true
        end

        def is_flavor_valid?
          service.get_flavor(config[:flavor]).nil? ? false : true
        end

        def is_floating_ip_valid?
          address = config[:openstack_floating_ip]

          return true if address == "-1" # no floating IP requested

          addresses = service.connection.addresses
          return false if addresses.empty? # no floating IPs

          # floating requested without value
          if address.nil?
            if addresses.find_index { |a| a.fixed_ip.nil? }
              true
            else
              false # no floating IPs available
            end
          else
            # floating requested with value
            if addresses.find_index { |a| a.ip == address }
              true
            else
              false # requested floating IP does not exist
            end
          end
        end

        def post_connection_validations
          errors = []
          errors << "You have not provided a valid image ID. Please note the options for this value are -I or --image." unless is_image_valid?
          errors << "You have not provided a valid flavor ID. Please note the options for this value are -f or --flavor." unless is_flavor_valid?
          errors << "You have either requested an invalid floating IP address or none are available." unless is_floating_ip_valid?
          error_message = ""
          raise CloudExceptions::ValidationError, error_message if errors.each { |e| ui.error(e); error_message = "#{error_message} #{e}." }.any?
        end

        def get_floating_ip_id(floating_address)
          # required for this method to work
          floating_ip_id = -1
          # Figure out the id for the port that the floating ip you requested
          @service.network.list_floating_ips[:body]["floatingips"].each do |x|
            if x["floating_ip_address"] == floating_address
              floating_ip_id = x["id"]
            end
          end
          floating_ip_id
        end
      end
    end
  end
end
