#!/usr/bin/env ruby

# -------------------------------------------------------------------------- #
# Copyright 2002-2015, OpenNebula Project (OpenNebula.org), C12G Labs        #
#                                                                            #
# 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.                                             #
#--------------------------------------------------------------------------- #

ONE_LOCATION=ENV["ONE_LOCATION"]

if !ONE_LOCATION
    RUBY_LIB_LOCATION="/usr/lib/one/ruby"
else
    RUBY_LIB_LOCATION=ONE_LOCATION+"/lib/ruby"
end

$: << RUBY_LIB_LOCATION
$: << RUBY_LIB_LOCATION+"/cli"

require 'command_parser'
require 'one_helper/oneuser_helper'
require 'one_helper/onequota_helper'

require 'uri'

cmd=CommandParser::CmdParser.new(ARGV) do
    usage "`oneuser` <command> [<args>] [<options>]"
    version OpenNebulaHelper::ONE_VERSION

    helper = OneUserHelper.new

    before_proc do
        helper.set_client(options) if @comm_name != :login
    end

    ########################################################################
    # Global Options
    ########################################################################
    set :option, CommandParser::OPTIONS+OpenNebulaHelper::CLIENT_OPTIONS

    list_options = CLIHelper::OPTIONS
    list_options << OpenNebulaHelper::XML
    list_options << OpenNebulaHelper::NUMERIC
    list_options << OpenNebulaHelper::DESCRIBE

    READ_FILE={
        :name => "read_file",
        :short => "-r",
        :large => "--read-file",
        :description => "Read password from file"
    }

    SHA1={
        :name => "sha1",
        :large => "--sha1",
        :description => "The password will be hashed using the sha1\n"<<
                        " "*31<<"algorithm"
    }

    SSH={
        :name => "ssh",
        :large => "--ssh",
        :description => "SSH Auth system",
        :proc => lambda { |o, options|
            options[:driver] = OpenNebula::User::SSH_AUTH
        }
    }

    X509={
        :name => "x509",
        :large => "--x509",
        :description => "x509 Auth system for x509 certificates",
        :proc => lambda { |o, options|
            options[:driver] = OpenNebula::User::X509_AUTH
        }
    }

    X509_PROXY={
        :name => "x509_proxy",
        :large => "--x509_proxy",
        :description => "x509 Auth system based on x509 proxy certificates",
        :proc => lambda { |o, options|
            options[:driver] = OpenNebula::User::X509_PROXY_AUTH
        }
    }

    KEY={
        :name => "key",
        :short => "-k path_to_private_key_pem",
        :large => "--key path_to_private_key_pem",
        :format => String,
        :description => "Path to the Private Key of the User"
    }

    CERT={
        :name => "cert",
        :short => "-c path_to_user_cert_pem",
        :large => "--cert path_to_user_cert_pem",
        :format => String,
        :description => "Path to the Certificate of the User"
    }

    PROXY={
        :name => "proxy",
        :large => "--proxy path_to_user_proxy_pem",
        :format => String,
        :description => "Path to the user proxy certificate"
    }

    TIME={
        :name  => "time",
        :large => "--time x",
        :format => Integer,
        :description => "Token duration in seconds, defaults to 36000 (10 h). "\
                        "To reset the token set time to 0." \
                        "To generate a non-expiring token use -1"\
                        " (not valid for ssh and x509 tokens). "\
    }

    DRIVER={
        :name => "driver",
        :large => "--driver driver",
        :format => String,
        :description => "Driver to autehnticate this user"
    }

    FORCE = {
        :name   => "force",
        :large  => "--force" ,
        :description => "Force one_auth file rewrite"
    }

    create_options = [READ_FILE, SHA1, SSH, X509, KEY, CERT, DRIVER]
    login_options  = [SSH, X509, X509_PROXY, KEY, CERT, PROXY, TIME, FORCE]

    ########################################################################
    # Formatters for arguments
    ########################################################################
    set :format, :groupid, OpenNebulaHelper.rname_to_id_desc("GROUP") do |arg|
        OpenNebulaHelper.rname_to_id(arg, "GROUP")
    end

    set :format, :userid, OneUserHelper.to_id_desc do |arg|
        helper.to_id(arg)
    end

    set :format, :userid_list, OneUserHelper.list_to_id_desc do |arg|
        helper.list_to_id(arg)
    end

    set :format, :password, OneUserHelper.password_to_str_desc do |arg|
        OneUserHelper.password_to_str(arg, options)
    end

    ########################################################################
    # Commands
    ########################################################################

    create_desc = <<-EOT.unindent
        Creates a new User
        Examples:
          oneuser create my_user my_password
          oneuser create my_user -r /tmp/mypass
          oneuser create my_user --ssh --key /tmp/id_rsa
          oneuser create my_user --ssh -r /tmp/public_key
          oneuser create my_user --x509 --cert /tmp/my_cert.pem
    EOT

    command :create, create_desc, :username, [:password, nil],
            :options=>create_options do
        if args[1]
            pass = args[1]
        else
            rc = helper.password(options)
            if rc.first == 0
                pass = rc[1]
            else
                exit_with_code *rc
            end
        end

        driver = options[:driver] || OpenNebula::User::CORE_AUTH

        helper.create_resource(options) do |user|
            user.allocate(args[0], pass, driver)
        end
    end

    update_desc = <<-EOT.unindent
        Update the template contents. If a path is not provided the editor will
        be launched to modify the current content.
    EOT

    command :update, update_desc, :userid, [:file, nil],
    :options=>OpenNebulaHelper::APPEND do
        helper.perform_action(args[0],options,"modified") do |obj|
            if options[:append]
                str = OpenNebulaHelper.append_template(args[0], obj, args[1])
            else
                str = OpenNebulaHelper.update_template(args[0], obj, args[1])
            end

            obj.update(str, options[:append])
        end
    end

    quota_desc = <<-EOT.unindent
        Set the quota limits for the user. If a path is not provided the editor
        will be launched to modify the current quotas.
    EOT

    command :quota, quota_desc, :userid, [:file, nil] do
        helper.perform_action(args[0], options, "modified") do |user|
            rc = user.info

            if OpenNebula.is_error?(rc)
                puts rc.message
                exit -1
            end

            str = OneQuotaHelper.set_quota(user, args[1])
            rc  = user.set_quota(str)

            if OpenNebula.is_error?(rc)
                puts rc.message
                exit -1
            end
        end
    end

    batchquota_desc = <<-EOT.unindent
        Sets the quota limits in batch for various users. If a path is not
        provided the editor will be launched to create new quotas.
    EOT

    command :batchquota, batchquota_desc, [:range, :userid_list],
            [:file, nil] do
        batch_str = OneQuotaHelper.get_batch_quota(args[1])

        helper.perform_actions(args[0], options, "modified") do |user|
            str = OneQuotaHelper.merge_quota(user, batch_str)

            if OpenNebula.is_error?(str)
                str
            else
                rc  = user.set_quota(str)
                rc
            end
        end
    end

    defaultquota_desc = <<-EOT.unindent
        Sets the default quota limits for the users. If a path is not provided
        the editor will be launched to modify the current default quotas.
    EOT

    command :defaultquota, defaultquota_desc, [:file, nil] do
        system = System.new(OneUserHelper.get_client(options))

        default_quotas = system.get_user_quotas()

        if OpenNebula.is_error?(default_quotas)
            puts default_quotas.message
            exit(-1)
        end

        str = OneQuotaHelper.set_quota(default_quotas, args[0], true)

        rc  = system.set_user_quotas(str)

        if OpenNebula.is_error?(rc)
            puts rc.message
            exit(-1)
        end

        exit 0
    end

    umask_desc = <<-EOT.unindent
        Changes the umask used to create the default permissions. In a similar
        way to the Unix umask command, the expected value is a three-digit
        base-8 number. Each digit is a mask that disables permissions for the
        owner, group and other, respectively.

        If mask is not given, or if it is an empty string, the umask will
        be unset
    EOT

    command :umask, umask_desc, [:range, :userid_list], [:mask, nil] do
        helper.perform_actions(args[0],options,
                "umask changed") do |user|

            rc = user.info

            if OpenNebula.is_error?(rc)
                puts rc.message
                exit -1
            end

            user.delete_element('/USER/TEMPLATE/UMASK')

            tmp_str = user.template_str

            if !args[1].nil? && args[1] != ""
                tmp_str << "\nUMASK = #{args[1]}"
            end

            user.update(tmp_str)
        end
    end

    login_desc = <<-EOT.unindent
        Creates the login token for authentication. The token can be used
        together with any authentication driver. The token will be stored in
        $HOME/.one/one_auth, and can be used subsequently to authenticate with
        oned through API, CLI or Sunstone.

        Example, request a valid token for a generic driver (e.g. core auth, LDAP...):
          oneuser login my_user --time 3600

        Example, generate and set a token for SSH based authentication:
          oneuser login my_user --ssh --key /tmp/id_rsa --time 72000

        Example, same using X509 certificates:
          oneuser login my_user --x509 --cert /tmp/my_cert.pem
                                --key /tmp/my_key.pk --time 72000

        Example, now with a X509 proxy certificate
          oneuser login my_user --x509_proxy --proxy /tmp/my_cert.pem
                                --time 72000
    EOT

    command :login, login_desc, :username, :options=>login_options do

        options[:time] ||= 36000

        helper.login(args[0], options)
    end

    key_desc = <<-EOT.unindent
        DEPRECATED, use login to generate auth files.

        Shows a public key from a private SSH key. Use it as password
        for the SSH authentication mechanism.
    EOT

    command :key, key_desc, :options=>[KEY] do
        require 'opennebula/ssh_auth'

        options[:key] ||= ENV['HOME']+'/.ssh/id_rsa'

        begin
            sshauth = SshAuth.new(:private_key=>options[:key])
        rescue Exception => e
            exit_with_code -1, e.message
        end

        puts sshauth.password
        exit_with_code 0
    end


    delete_desc = <<-EOT.unindent
        Deletes the given User
    EOT

    command :delete, delete_desc, [:range, :userid_list] do
        helper.perform_actions(args[0], options, "deleted") do |user|
            user.delete
        end
    end

    passwd_desc = <<-EOT.unindent
        Changes the given User's password
    EOT

    command :passwd, passwd_desc, :userid, [:password, nil],
            :options=>create_options do
        if args[1]
            pass = args[1]
        else
            rc = helper.password(options)
            if rc.first == 0
                pass = rc[1]
            else
                exit_with_code *rc
            end
        end

        helper.perform_action(args[0],options,"Password changed") do |user|
            user.passwd(pass)
        end
    end

    chgrp_desc = <<-EOT.unindent
        Changes the User's primary group
    EOT

    command :chgrp, chgrp_desc, [:range, :userid_list], :groupid do
        helper.perform_actions(args[0],options,"Group changed") do |user|
            user.chgrp(args[1].to_i)
        end
    end

    addgroup_desc = <<-EOT.unindent
        Adds the User to a secondary group
    EOT

    command :addgroup, addgroup_desc, [:range, :userid_list], :groupid do
        gid = args[1]

        helper.perform_actions(args[0],options,"group added") do |user|
            user.addgroup( gid )
        end
    end

    delgroup_desc = <<-EOT.unindent
        Removes the User from a secondary group
    EOT

    command :delgroup, delgroup_desc, [:range, :userid_list], :groupid do
        gid = args[1]

        helper.perform_actions(args[0],options,"group deleted") do |user|
            user.delgroup( gid )
        end
    end

    chauth_desc = <<-EOT.unindent
        Changes the User's auth driver and its password (optional)
        Examples:
          oneuser chauth my_user core
          oneuser chauth my_user core new_password
          oneuser chauth my_user core -r /tmp/mypass
          oneuser chauth my_user --ssh --key /home/oneadmin/.ssh/id_rsa
          oneuser chauth my_user --ssh -r /tmp/public_key
          oneuser chauth my_user --x509 --cert /tmp/my_cert.pem
    EOT

    command :chauth, chauth_desc, :userid, [:auth, nil], [:password, nil],
            :options=>create_options do
        if options[:driver]
            driver = options[:driver]
        elsif args[1]
            driver = args[1]
        else
            exit_with_code 0, "An Auth driver should be specified"
        end

        if args[2]
            pass = args[2]
        else
            rc = helper.password(options)
            if rc.first == 0
                pass = rc[1]
            else
                pass = ""
            end
        end

        helper.perform_action(args[0],
                            options,
                            "Auth driver and password changed") do |user|
            user.chauth(driver, pass)
        end
    end

    list_desc = <<-EOT.unindent
        Lists Users in the pool
    EOT

    command :list, list_desc, :options=>list_options do
        helper.list_pool(options)
    end

    show_desc = <<-EOT.unindent
        Shows information for the given User
    EOT

    command :show, show_desc, [:userid, nil],
            :options=>OpenNebulaHelper::XML do
        user=args[0] || OpenNebula::User::SELF
        helper.show_resource(user,options)
    end

    show_desc = <<-EOT.unindent
        Encodes user and password to use it with ldap
    EOT

    command :encode, show_desc, :username, [:password, nil] do
        ar=args.compact

        if defined?(URI::Parser)
            parser=URI::Parser.new
        else
            parser=URI
        end

        puts ar.map{|a| parser.escape(a) }.join(':')

        0
    end
end
