class Morpheus::Cli::BudgetsCommand

Public Instance Methods

add(args) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 222
  def add(args)
    options = {}
    params = {}
    costs = []
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[name] [options]")
      build_option_type_options(opts, options, add_budget_option_types)
      # opts.on('--cost [amount]', String, "Budget cost amount, for use with default year interval.") do |val|
      #   costs['year'] = (val.nil? || val.empty?) ? 0 : val.to_f
      # end
      opts.on('--costs LIST', String, "Budget cost amounts, one for each interval in the budget. eg \"350\" for one year, \"25,25,25,100\" for quarters, and \"10,10,10,10,10,10,10,10,10,10,10,50\" for each month") do |val|
        val = val.to_s.gsub('[', '').gsub(']', '')
        costs = val.to_s.split(',').collect {|it| parse_cost_amount(it) }
      end
      (1..12).each.with_index do |cost_index, i|
        opts.on("--cost#{cost_index} VALUE", String, "Cost #{cost_index.to_s.capitalize} amount") do |val|
          #params["cost#{cost_index.to_s}"] = parse_cost_amount(val)
          costs[i] = parse_cost_amount(val)
        end
        opts.add_hidden_option("--cost#{cost_index}")
      end
      [:q1,:q2,:q3,:q4,].each.with_index do |quarter, i|
        opts.on("--#{quarter.to_s} VALUE", String, "#{quarter.to_s.capitalize} cost amount, use with quarter interval.") do |val|
          costs[i] = parse_cost_amount(val)
        end
        opts.add_hidden_option("--#{quarter.to_s}")
      end
      [:january,:february,:march,:april,:may,:june,:july,:august,:september,:october,:november,:december].each_with_index do |month, i|
        opts.on("--#{month.to_s} VALUE", String, "#{month.to_s.capitalize} cost amount, use with month interval.") do |val|
          costs[i] = parse_cost_amount(val)
        end
        opts.add_hidden_option("--#{month.to_s}")
      end
      opts.on('--enabled [on|off]', String, "Can be used to disable a policy") do |val|
        params['enabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s.empty?
      end
      build_standard_add_options(opts, options)
      opts.footer = <<-EOT
Create a budget.
The default period is the current year, eg. "#{Time.now.year}"
and the default interval is "year".
Costs can be passed as an array of values, one for each interval. eg. --costs "[999]"

Examples:
budgets add example-budget --interval "year" --costs "[2500]"
budgets add example-qtr-budget --interval "quarter" --costs "[500,500,500,1000]"
budgets add example-monthly-budget --interval "month" --costs "[400,100,100,100,100,100,100,100,100,100,400,800]"
budgets add example-future-budget --year "2022" --interval "year" --costs "[5000]"
budgets add example-custom-budget --year "custom" --interval "year" --start "2021-01-01" --end "2023-12-31" --costs "[2500,5000,10000]"
EOT
    end
    optparse.parse!(args)
    if args.count > 1
      raise_command_error "wrong number of arguments, expected 0-1 and got (#{args.count}) #{args}\n#{optparse}"
    end
    if args[0]
      options[:options]['name'] ||= args[0]
    end
    connect(options)
    begin
      # construct payload
      passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
      payload = nil
      if options[:payload]
        payload = options[:payload]
        payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
      else
        payload = {
          'budget' => {
          }
        }
        # allow arbitrary -O options
        #passed_options.delete('costs')
        passed_options.delete('tenant')
        passed_options.delete('group')
        passed_options.delete('cloud')
        passed_options.delete('user')
        payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
        # prompt for options
        v_prompt = Morpheus::Cli::OptionTypes.prompt(add_budget_option_types, options[:options], @api_client)
        params.deep_merge!(v_prompt)
        # downcase year 'custom' always
        if params['year']
          params['interval'] = params['interval'].to_s.downcase
        end
        # downcase interval always
        if params['interval']
          params['interval'] = params['interval'].to_s.downcase
        end
        # parse MM/DD/YY but need to convert to to ISO format YYYY-MM-DD for api
        standard_start_date = (params['startDate'] ? Time.strptime(params['startDate'], "%x") : nil) rescue nil
        if standard_start_date
          params['startDate'] = format_date(standard_start_date, {format:"%Y-%m-%d"})
        end
        standard_end_date = (params['endDate'] ? Time.strptime(params['endDate'], "%x") : nil) rescue nil
        if standard_end_date
          params['endDate'] = format_date(standard_end_date, {format:"%Y-%m-%d"})
        end
        if !costs.empty?
          params['costs'] = costs
        else
          params['costs'] = prompt_costs(params, options)
        end
        # budgets api expects scope prefixed parameters like this
        if params['tenant'].is_a?(String) || params['tenant'].is_a?(Numeric)
          params['scopeTenantId'] = params.delete('tenant')
        end
        if params['group'].is_a?(String) || params['group'].is_a?(Numeric)
          params['scopeGroupId'] = params.delete('group')
        end
        if params['cloud'].is_a?(String) || params['cloud'].is_a?(Numeric)
          params['scopeCloudId'] = params.delete('cloud')
        end
        if params['user'].is_a?(String) || params['user'].is_a?(Numeric)
          params['scopeUserId'] = params.delete('user')
        end
        payload.deep_merge!({'budget' => params}) unless params.empty?
      end

      @budgets_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @budgets_interface.dry.create(payload)
        return
      end
      json_response = @budgets_interface.create(payload)
      if options[:json]
        print JSON.pretty_generate(json_response)
        print "\n"
      else
        display_name = json_response['budget']  ? json_response['budget']['name'] : ''
        print_green_success "Budget #{display_name} added"
        get([json_response['budget']['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
      end
      return 0
    rescue RestClient::Exception => e
      print_rest_exception(e, options)
      exit 1
    end
  end
connect(opts) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 10
def connect(opts)
  @api_client = establish_remote_appliance_connection(opts)
  @budgets_interface = @api_client.budgets
  @options_interface = @api_client.options
end
get(args) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 93
def get(args)
  options = {}
  params = {}
  optparse = Morpheus::Cli::OptionParser.new do |opts|
    opts.banner = subcommand_usage("[budget]")
    build_standard_get_options(opts, options)
    opts.footer = "Get details about a budget.\n[budget] is required. Budget ID or name"
  end
  optparse.parse!(args)
  verify_args!(args:args, optparse:optparse, count:1)
  connect(options)
  params.merge!(parse_query_options(options))
  @budgets_interface.setopts(options)
  if options[:dry_run]
    if args[0].to_s =~ /\A\d{1,}\Z/
      print_dry_run @budgets_interface.dry.get(args[0], params)
    else
      print_dry_run @budgets_interface.dry.list({name: args[0].to_s})
    end
    return 0
  end
  budget = find_budget_by_name_or_id(args[0])
  return 1 if budget.nil?
  # skip reload if already fetched via get(id)
  json_response = {'budget' => budget}
  if args[0].to_s != budget['id'].to_s
    json_response = @budgets_interface.get(budget['id'], params)
    budget = json_response['budget']
  end
  
  render_response(json_response, options, 'budget') do
    
    print_h1 "Budget Details"
    print cyan
    budget_columns = {
      "ID" => 'id',
      "Name" => 'name',
      "Description" => 'description',
      "Enabled" => lambda {|budget| format_boolean(budget['enabled']) },
      "Scope" => lambda {|it| format_budget_scope(it) },
      "Period" => lambda {|it| it['year'] },
      "Interval" => lambda {|it| it['interval'].to_s.capitalize },
      "Forecast Model" => lambda {|it| it['forecastType'] ? it['forecastType']['name'] : '' },
      # the UI doesn't consider timezone, so uhh do it this hacky way for now.
      "Start Date" => lambda {|it| 
        if it['timezone'] == 'UTC'
          ((parse_time(it['startDate'], "%Y-%m-%d").strftime("%x")) rescue it['startDate']) # + ' UTC'
        else
          format_local_date(it['startDate']) 
        end
      },
      "End Date" => lambda {|it| 
        if it['timezone'] == 'UTC'
          ((parse_time(it['endDate'], "%Y-%m-%d").strftime("%x")) rescue it['endDate']) # + ' UTC'
        else
          format_local_date(it['endDate']) 
        end
      },
      # "Costs" => lambda {|it|
      #   if it['costs'].is_a?(Array)
      #     it['costs'] ? it['costs'].join(', ') : ''
      #   elsif it['costs'].is_a?(Hash)
      #     it['costs'].to_s
      #   else
      #     it['costs'].to_s
      #   end
      # },
      "Created By" => lambda {|it| it['createdByName'] ? it['createdByName'] : it['createdById'] },
      "Created" => lambda {|it| format_local_dt(it['dateCreated']) },
      "Updated" => lambda {|it| format_local_dt(it['lastUpdated']) },
    }
    print_description_list(budget_columns, budget, options)
    # print reset,"\n"

    # a chart of Budget cost vs Actual cost for each interval in the period.
    print_h2 "Budget Summary", options
    if budget['stats'] && budget['stats']['intervals']
      begin
        budget_summary_columns = {
          # "Cost" => lambda {|it| it[:label] },
          " " => lambda {|it| it[:label] },
        }
        budget_row = {label:"Budget"}
        actual_row = {label:"Actual"}
        forecast_row = {label:"Forecast"}
        multi_year = false
        if budget['startDate'] && budget['endDate'] && parse_time(budget['startDate']).year != parse_time(budget['endDate']).year
          multi_year = true
        end
        budget['stats']['intervals'].each do |it|
          currency = budget['currency'] || budget['stats']['currency']
          interval_key = format_budget_interval_label(budget, it).to_s.upcase
          # interval_date = parse_time(it["startDate"]) rescue nil
          budget_summary_columns[interval_key] = interval_key
          budget_cost = it["budget"].to_f
          actual_cost = it["cost"].to_f
          over_budget = actual_cost > 0 && actual_cost > budget_cost
          if over_budget
            budget_row[interval_key] = "#{cyan}#{format_money(budget_cost, currency)}#{cyan}"
            actual_row[interval_key] = "#{red}#{format_money(actual_cost, currency)}#{cyan}"
          else
            budget_row[interval_key] = "#{cyan}#{format_money(budget_cost, currency)}#{cyan}"
            actual_row[interval_key] = "#{cyan}#{format_money(actual_cost, currency)}#{cyan}"
          end
          forecast_cost = it["forecast"] ? it["forecast"].to_f : 0
          forecast_over_budget = forecast_cost > 0 && forecast_cost > budget_cost
          if forecast_over_budget
            forecast_row[interval_key] = "#{red}#{format_money(forecast_cost, currency)}#{cyan}"
          else
            forecast_row[interval_key] = "#{cyan}#{format_money(forecast_cost, currency)}#{cyan}"
          end
        end
        chart_data = [budget_row, actual_row]
        if budget['stats']['intervals'].find { |it| it['forecast'] && it['forecast'] != 0 }
          chart_data << forecast_row
        end
        print as_pretty_table(chart_data, budget_summary_columns, options)
        print reset,"\n"
      rescue => ex
        print red,"Failed to render budget summary.",reset,"\n"
        raise ex
      end
    else
      print cyan,"No budget stat data found.",reset,"\n"
    end
  end
  return 0, nil
end
handle(args) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 16
def handle(args)
  handle_subcommand(args)
end
list(args) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 20
def list(args)
  options = {}
  params = {}
  optparse = Morpheus::Cli::OptionParser.new do |opts|
    opts.banner = subcommand_usage()
    build_standard_list_options(opts, options)
    opts.footer = "List budgets."
  end
  optparse.parse!(args)
  # verify_args!(args:args, optparse:optparse, count:0)
  if args.count > 0
    options[:phrase] = args.join(" ")
  end
  params.merge!(parse_list_options(options))
  connect(options)
  
  params.merge!(parse_list_options(options))
  @budgets_interface.setopts(options)
  if options[:dry_run]
    print_dry_run @budgets_interface.dry.list(params)
    return 0
  end
  json_response = @budgets_interface.list(params)
  budgets = json_response['budgets']
  render_response(json_response, options, 'budgets') do
    title = "Morpheus Budgets"
    subtitles = []
    subtitles += parse_list_subtitles(options)
    print_h1 title, subtitles
    if budgets.empty?
      print cyan,"No budgets found.",reset,"\n"
    else
      columns = [
        {"ID" => lambda {|budget| budget['id'] } },
        {"NAME" => lambda {|budget| budget['name'] } },
        {"DESCRIPTION" => lambda {|budget| truncate_string(budget['description'], 30) } },
        # {"ENABLED" => lambda {|budget| format_boolean(budget['enabled']) } },
        # {"SCOPE" => lambda {|it| format_budget_scope(it) } },
        {"SCOPE" => lambda {|it| it['refName'] } },
        {"PERIOD" => lambda {|it| it['year'] } },
        {"INTERVAL" => lambda {|it| it['interval'].to_s.capitalize } },
        # the UI doesn't consider timezone, so uhh do it this hacky way for now.
        {"START DATE" => lambda {|it| 
          if it['timezone'] == 'UTC'
            ((parse_time(it['startDate'], "%Y-%m-%d").strftime("%x")) rescue it['startDate']) # + ' UTC'
          else
            format_local_date(it['startDate']) 
          end
        } },
        {"END DATE" => lambda {|it| 
          if it['timezone'] == 'UTC'
            ((parse_time(it['endDate'], "%Y-%m-%d").strftime("%x")) rescue it['endDate']) # + ' UTC'
          else
            format_local_date(it['endDate']) 
          end
        } },
        {"TOTAL" => lambda {|it| format_money(it['totalCost'], it['currency']) } },
        {"AVERAGE" => lambda {|it| format_money(it['averageCost'], it['currency']) } },
        # {"CREATED BY" => lambda {|budget| budget['createdByName'] ? budget['createdByName'] : budget['createdById'] } },
        # {"CREATED" => lambda {|budget| format_local_dt(budget['dateCreated']) } },
        # {"UPDATED" => lambda {|budget| format_local_dt(budget['lastUpdated']) } },
      ]
      if options[:include_fields]
        columns = options[:include_fields]
      end
      print as_pretty_table(budgets, columns, options)
      print_results_pagination(json_response)
    end
    print reset,"\n"
  end
  return budgets.empty? ? [3, "no budgets found"] : [0, nil]
end
remove(args) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 507
def remove(args)
  options = {}
  optparse = Morpheus::Cli::OptionParser.new do |opts|
    opts.banner = subcommand_usage("[name]")
    build_common_options(opts, options, [:auto_confirm, :json, :dry_run, :remote])
    opts.footer = "Delete budget.\n[budget] is required. Budget ID or name"
  end
  optparse.parse!(args)

  if args.count != 1
    raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
  end

  connect(options)
  begin
    budget = find_budget_by_name_or_id(args[0])
    return 1 if budget.nil?

    unless options[:yes] || Morpheus::Cli::OptionTypes.confirm("Are you sure you want to delete the budget #{budget['name']}?")
      return 9, "aborted command"
    end
    @budgets_interface.setopts(options)
    if options[:dry_run]
      print_dry_run @budgets_interface.dry.destroy(budget['id'])
      return
    end
    json_response = @budgets_interface.destroy(budget['id'])
    if options[:json]
      print JSON.pretty_generate(json_response)
      print "\n"
    else
      print_green_success "Budget #{budget['name']} removed"
      # list([] + (options[:remote] ? ["-r",options[:remote]] : []))
    end
    return 0
  rescue RestClient::Exception => e
    print_rest_exception(e, options)
    exit 1
  end
end
update(args) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 362
  def update(args)
    options = {}
    params = {}
    costs = []
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[budget] [options]")
      build_option_type_options(opts, options, update_budget_option_types)
      # opts.on('--cost [amount]', String, "Budget cost amount, for use with default year interval.") do |val|
      #   costs['year'] = (val.nil? || val.empty?) ? 0 : val.to_f
      # end
      opts.on('--costs COSTS', String, "Budget cost amounts, one for each interval in the budget. eg. [999]") do |val|
        val = val.to_s.gsub('[', '').gsub(']', '')
        costs = val.to_s.split(',').collect {|it| parse_cost_amount(it) }
      end
      (1..12).each.with_index do |cost_index, i|
        opts.on("--cost#{cost_index} VALUE", String, "Cost #{cost_index.to_s.capitalize} amount") do |val|
          #params["cost#{cost_index.to_s}"] = parse_cost_amount(val)
          costs[i] = parse_cost_amount(val)
        end
        opts.add_hidden_option("--cost#{cost_index}")
      end
      [:q1,:q2,:q3,:q4,].each.with_index do |quarter, i|
        opts.on("--#{quarter.to_s} VALUE", String, "#{quarter.to_s.capitalize} cost amount, use with quarter interval.") do |val|
          costs[i] = parse_cost_amount(val)
        end
        opts.add_hidden_option("--#{quarter.to_s}")
      end
      [:january,:february,:march,:april,:may,:june,:july,:august,:september,:october,:november,:december].each_with_index do |month, i|
        opts.on("--#{month.to_s} VALUE", String, "#{month.to_s.capitalize} cost amount, use with month interval.") do |val|
          costs[i] = parse_cost_amount(val)
        end
        opts.add_hidden_option("--#{month.to_s}")
      end
      opts.on('--enabled [on|off]', String, "Can be used to disable a policy") do |val|
        params['enabled'] = val.to_s == 'on' || val.to_s == 'true' || val.to_s.empty?
      end
      build_standard_update_options(opts, options)
      opts.footer = <<-EOT
Update a budget.
[budget] is required. Budget ID or name
EOT
      opts.footer = "Update a budget.\n[budget] is required. Budget ID or name"
    end
    optparse.parse!(args)

    if args.count != 1
      raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args}\n#{optparse}"
    end

    connect(options)
    
    budget = find_budget_by_name_or_id(args[0])
    return 1 if budget.nil?

    original_year = budget['year']
    original_interval = budget['interval']
    original_costs = budget['costs'].is_a?(Array) ? budget['costs'] : nil

    # construct payload
    passed_options = options[:options] ? options[:options].reject {|k,v| k.is_a?(Symbol) } : {}
    payload = nil
    if options[:payload]
      payload = options[:payload]
      payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
    else
      payload = {
        'budget' => {
        }
      }
      # allow arbitrary -O options
      #passed_options.delete('costs')
      passed_options.delete('tenant')
      passed_options.delete('group')
      passed_options.delete('cloud')
      passed_options.delete('user')
      payload.deep_merge!({'budget' => passed_options}) unless passed_options.empty?
      # prompt for options
      #params = Morpheus::Cli::OptionTypes.prompt(update_budget_option_types, options[:options], @api_client, options[:params])
      v_prompt = Morpheus::Cli::OptionTypes.prompt(update_budget_option_types, options[:options].merge(:no_prompt => true), @api_client)
      params.deep_merge!(v_prompt)
      # downcase year 'custom' always
      if params['year']
        params['interval'] = params['interval'].to_s.downcase
      end
      # downcase interval always
      if params['interval']
        params['interval'] = params['interval'].to_s.downcase
      end
      # parse MM/DD/YY but need to convert to to ISO format YYYY-MM-DD for api
      if params['startDate']
        params['startDate'] = format_date(parse_time(params['startDate']), {format:"%Y-%m-%d"})
      end
      if params['endDate']
        params['endDate'] = format_date(parse_time(params['endDate']), {format:"%Y-%m-%d"})
      end
      if !costs.empty?
        params['costs'] = costs
        # merge original costs in on update unless interval is changing too, should check original_year too probably if going to custom...
        if params['interval'] && params['interval'] != original_interval
          original_costs = nil
        end
        if original_costs
          original_costs.each_with_index do |original_cost, i|
            if params['costs'][i].nil?
              params['costs'][i] = original_cost
            end
          end
        end
      else
        if params['interval'] && params['interval'] != original_interval
          raise_command_error "Changing interval requires setting the costs as well.\n#{optparse}"
        end
      end
      # budgets api expects scope prefixed parameters like this
      if params['tenant'].is_a?(String) || params['tenant'].is_a?(Numeric)
        params['scopeTenantId'] = params.delete('tenant')
      end
      if params['group'].is_a?(String) || params['group'].is_a?(Numeric)
        params['scopeGroupId'] = params.delete('group')
      end
      if params['cloud'].is_a?(String) || params['cloud'].is_a?(Numeric)
        params['scopeCloudId'] = params.delete('cloud')
      end
      if params['user'].is_a?(String) || params['user'].is_a?(Numeric)
        params['scopeUserId'] = params.delete('user')
      end
      payload.deep_merge!({'budget' => params}) unless params.empty?
      if payload.empty? || payload['budget'].empty?
        raise_command_error "Specify at least one option to update.\n#{optparse}"
      end
    end
    @budgets_interface.setopts(options)
    if options[:dry_run]
      print_dry_run @budgets_interface.dry.update(budget['id'], payload)
      return
    end
    json_response = @budgets_interface.update(budget['id'], payload)
    render_response(json_response, options, 'budget') do
      display_name = json_response['budget'] ? json_response['budget']['name'] : ''
      print_green_success "Budget #{display_name} updated"
      get([json_response['budget']['id']] + (options[:remote] ? ["-r",options[:remote]] : []))
    end
    return 0, nil
  end

Private Instance Methods

add_budget_option_types() click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 588
def add_budget_option_types
  [
    {'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true},
    {'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text'},
    # {'fieldName' => 'enabled', 'fieldLabel' => 'Enabled', 'type' => 'checkbox', 'defaultValue' => true},
    {'fieldName' => 'scope', 'fieldLabel' => 'Scope', 'code' => 'budget.scope', 'type' => 'select', 'selectOptions' => [{'name'=>'Account','value'=>'account'},{'name'=>'Tenant','value'=>'tenant'},{'name'=>'Cloud','value'=>'cloud'},{'name'=>'Group','value'=>'group'},{'name'=>'User','value'=>'user'}], 'defaultValue' => 'account', 'required' => true},
    {'fieldName' => 'tenant', 'fieldLabel' => 'Tenant', 'type' => 'select', 'optionSource' => lambda {|api_client, api_params| 
      @options_interface.options_for_source("tenants", {})['data']
    }, 'required' => true, 'dependsOnCode' => 'budget.scope:tenant'},
    {'fieldName' => 'user', 'fieldLabel' => 'User', 'type' => 'select', 'optionSource' => lambda {|api_client, api_params|
      @options_interface.options_for_source("users", {})['data']
    }, 'required' => true, 'dependsOnCode' => 'budget.scope:user'},
    {'fieldName' => 'group', 'fieldLabel' => 'Group', 'type' => 'select', 'optionSource' => lambda {|api_client, api_params| 
      @options_interface.options_for_source("groups", {})['data']
    }, 'required' => true, 'dependsOnCode' => 'budget.scope:group'},
    {'fieldName' => 'cloud', 'fieldLabel' => 'Cloud', 'type' => 'select', 'optionSource' => lambda {|api_client, api_params| 
      @options_interface.options_for_source("clouds", {})['data']
    }, 'required' => true, 'dependsOnCode' => 'budget.scope:cloud'},
    {'fieldName' => 'year', 'fieldLabel' => 'Period', 'code' => 'budget.year', 'type' => 'text', 'required' => true, 'defaultValue' => Time.now.year, 'description' => "The period (year) the budget applies, YYYY or 'custom' to enter Start Date and End Date manually"},
    {'fieldName' => 'startDate', 'fieldLabel' => 'Start Date', 'type' => 'text', 'required' => true, 'description' => 'The Start Date for custom period budget eg. 2021-01-01', 'dependsOnCode' => 'budget.year:custom'},
    {'fieldName' => 'endDate', 'fieldLabel' => 'End Date', 'type' => 'text', 'required' => true, 'description' => 'The End Date for custom period budget eg. 2023-12-31 (must be 1, 2 or 3 years from Start Date)', 'dependsOnCode' => 'budget.year:custom'},
    {'fieldName' => 'interval', 'fieldLabel' => 'Interval', 'type' => 'select', 'selectOptions' => [{'name'=>'Year','value'=>'year'},{'name'=>'Quarter','value'=>'quarter'},{'name'=>'Month','value'=>'month'}], 'defaultValue' => 'year', 'required' => true, 'description' => 'The budget interval, determines cost amounts: "year", "quarter" or "month"'},
    {'fieldName' => 'forecastType.id', 'fieldLabel' => 'Forecast Model', 'type' => 'select', 'optionSource' => 'forecastTypes', 'description' => 'The budget forcecast model type, determines projected cost calculations.'},
  ]
end
find_budget_by_id(id) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 558
def find_budget_by_id(id)
  raise "#{self.class} has not defined @budgets_interface" if @budgets_interface.nil?
  begin
    json_response = @budgets_interface.get(id)
    return json_response['budget']
  rescue RestClient::Exception => e
    if e.response && e.response.code == 404
      print_red_alert "Budget not found by id #{id}"
    else
      raise e
    end
  end
end
find_budget_by_name(name) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 572
def find_budget_by_name(name)
  raise "#{self.class} has not defined @budgets_interface" if @budgets_interface.nil?
  budgets = @budgets_interface.list({name: name.to_s})['budgets']
  if budgets.empty?
    print_red_alert "Budget not found by name #{name}"
    return nil
  elsif budgets.size > 1
    print_red_alert "#{budgets.size} Budgets found by name #{name}"
    print as_pretty_table(budgets, [:id,:name], {color:red})
    print reset,"\n"
    return nil
  else
    return budgets[0]
  end
end
find_budget_by_name_or_id(val) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 550
def find_budget_by_name_or_id(val)
  if val.to_s =~ /\A\d{1,}\Z/
    return find_budget_by_id(val)
  else
    return find_budget_by_name(val)
  end
end
format_budget_interval_label(budget, budget_interval) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 745
def format_budget_interval_label(budget, budget_interval)
  label = ""
  if budget_interval['chartName']
    label = budget_interval['chartName']
  else
    label = (budget_interval['shortName'] || budget_interval['shortYear']).to_s
  end
  return label
end
format_budget_scope(budget) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 730
def format_budget_scope(budget)
  if budget['refScope'] && budget['refName']
    "(#{budget['refScope']}) #{budget['refName']}"
  elsif budget['refType']
    budget['refType'] ? "#{budget['refType']} (#{budget['refId']}) #{budget['refName']}".strip : budget['refName'].to_s
  else
    ""
  end
end
parse_cost_amount(val) click to toggle source

convert String like “$5,499.99” to Float 5499.99

# File lib/morpheus/cli/commands/budgets_command.rb, line 741
def parse_cost_amount(val)
  val.to_s.gsub(",","").gsub('$','').strip.to_f
end
prompt_costs(params={}, options={}) click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 625
def prompt_costs(params={}, options={})
  # user did -O costs="[3.50,3.50,3.50,5.00]" so just pass through
  default_costs = []
  if options[:options]['costs'] && options[:options]['costs'].is_a?(Array)
    default_costs = options[:options]['costs']
    default_costs.each_with_index do |default_cost, i|
      interval_index =  i + 1
      if !default_cost.nil?
        options[:options]["cost#{interval_index}"] = default_cost
      end
    end
  end
  # prompt for each Period Cost based on interval [year|quarter|month]
  budget_period_year = (params['year'] || params['periodValue'])
  is_custom = budget_period_year == 'custom'
  interval = params['interval'] #.to_s.downcase
  total_years = 1
  total_months = 12
  costs = []
  # custom timeframe so prompt from start to end by interval
  start_date = nil
  end_date = nil

  if is_custom
    start_date = parse_time(params['startDate'])
    if start_date.nil?
      raise_command_error "startDate is required for custom period budgets"
    end
    end_date = parse_time(params['endDate'])
    if end_date.nil?
      raise_command_error "endDate is required for custom period budgets"
    end
  else
    budget_year = budget_period_year ? budget_period_year.to_i : Time.now.year.to_i
    start_date = Time.new(budget_year, 1, 1)
    end_date = Time.new(budget_year, 12, 31)
  end
  epoch_start_month = (start_date.year * 12) + start_date.month
  epoch_end_month = (end_date.year * 12) + end_date.month
  # total_months gets + 1 because endDate is same year, on last day of the month, Dec 31 by default
  total_months = (epoch_end_month - epoch_start_month) + 1
  total_years = (total_months / 12)
  cost_option_types = []
  interval_count = total_months
  if interval == 'year'
    interval_count = total_months / 12
  elsif interval == 'quarter'
    interval_count = total_months / 3
  end
  
  is_fiscal = start_date.month != 1 || start_date.day != 1

  # debug budget shenanigans
  # puts "START: #{start_date}"
  # puts "END: #{end_date}"
  # puts "EPOCH MONTHS: #{epoch_start_month} - #{epoch_end_month}"
  # puts "TOTAL MONTHS: #{total_months}"
  # puts "INTERVAL COUNT IS: #{interval_count}"

  if total_months < 0
    raise_command_error "budget cannot end (#{end_date}) before it starts (#{start_date})"
  end
  if (total_months % 12) != 0 || (total_months > 36)
    raise_command_error "budget custom period must be 12, 24, or 36 months."
  end
  if interval == 'year'
    (1..interval_count).each_with_index do |interval_index, i|
      interval_start_month = epoch_start_month + (i * 12)
      interval_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
      display_year = is_fiscal ? "FY #{interval_date.year + 1}" : interval_date.year.to_s
      field_name = "cost#{interval_index}"
      field_label = "#{display_year} Cost"
      cost_option_types << {'fieldName' => field_name, 'fieldLabel' => field_label, 'type' => 'text', 'required' => true, 'defaultValue' => (default_costs[i] || 0).to_s}
    end
  elsif interval == 'quarter'
    (1..interval_count).each_with_index do |interval_index, i|
      interval_start_month = epoch_start_month + (i * 3)
      interval_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
      interval_end_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
      display_year = is_fiscal ? "FY #{interval_date.year + 1}" : interval_date.year.to_s
      field_name = "cost#{interval_index}"
      # field_label = "Q#{interval_index} Cost"
      field_label = "Q#{(i % 4) + 1} #{display_year} Cost"
      cost_option_types << {'fieldName' => field_name, 'fieldLabel' => field_label, 'type' => 'text', 'required' => true, 'defaultValue' => (default_costs[i] || 0).to_s}
    end
  elsif interval == 'month'
    (1..interval_count).each_with_index do |interval_index, i|
      interval_start_month = epoch_start_month + i
      interval_date = Time.new((interval_start_month / 12), (interval_start_month % 12) == 0 ? 12 : (interval_start_month % 12), 1)
      display_year = is_fiscal ? "FY #{interval_date.year + 1}" : interval_date.year.to_s
      field_name = "cost#{interval_index}"
      # field_label = "#{interval_date.strftime('%B %Y')} Cost"
      field_label = "#{interval_date.strftime('%B')} #{display_year} Cost"
      cost_option_types << {'fieldName' => field_name, 'fieldLabel' => field_label, 'type' => 'text', 'required' => true, 'defaultValue' => (default_costs[i] || 0).to_s}
    end
  end
  # values is a Hash like {"cost1": 99.0, "cost2": 55.0}
  values = Morpheus::Cli::OptionTypes.prompt(cost_option_types, options[:options], @api_client)
  values.each do |k,v|
    interval_index = k[4..-1].to_i
    costs[interval_index-1] = parse_cost_amount(v).to_f
  end
  return costs
end
update_budget_option_types() click to toggle source
# File lib/morpheus/cli/commands/budgets_command.rb, line 614
def update_budget_option_types
  list = add_budget_option_types()
  # list = list.reject {|it| ["interval"].include? it['fieldName'] }
  list.each {|it| 
    it.delete('required') 
    it.delete('defaultValue')
    it.delete('dependsOnCode')
  }
  list
end