<commit_before># require 'yaml'
require 'io/console'
require 'rest_client'
require 'optparse'
require 'filesize'
require 'morpheus/cli/cli_command'
require 'morpheus/cli/mixins/infrastructure_helper'

class Morpheus::Cli::SecurityGroups
  include Morpheus::Cli::CliCommand
  include Morpheus::Cli::InfrastructureHelper

  register_subcommands :list, :get, :add, :update, :remove
  register_subcommands :'add-location', :'remove-location'
  register_subcommands :'add-rule', :'update-rule', :'remove-rule'
  set_default_subcommand :list
  
  def connect(opts)
    @api_client = establish_remote_appliance_connection(opts)
    @security_groups_interface = @api_client.security_groups
    @security_group_rules_interface = @api_client.security_group_rules
    @cloud_resource_pools_interface = @api_client.cloud_resource_pools
    @clouds_interface = @api_client.clouds
    @options_interface = @api_client.options
    @active_security_group = ::Morpheus::Cli::SecurityGroups.load_security_group_file
    @network_security_servers = @api_client.network_security_servers
  end

  def handle(args)
    handle_subcommand(args)
  end


  def list(args)
    options = {}
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage()
      build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
      opts.footer = "List security groups."
    end
    optparse.parse!(args)
    if args.count != 0
      raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    connect(options)
    begin
      params = {}
      params.merge!(parse_list_options(options))
      @security_groups_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @security_groups_interface.dry.list(params)
        return
      end
      json_response = @security_groups_interface.list(params)
      
      render_result = render_with_format(json_response, options, 'securityGroups')
      return 0 if render_result

      title = "Morpheus Security Groups"
      subtitles = []
      subtitles += parse_list_subtitles(options)
      print_h1 title, subtitles

      security_groups = json_response['securityGroups']
      
      if security_groups.empty?
        print yellow,"No security groups found.",reset,"\n"
      else
        active_id = @active_security_group[@appliance_name.to_sym]
        # table_color = options[:color] || cyan
        # rows = security_groups.collect do |security_group|
        #   {
        #     id: security_group['id'].to_s + ((security_group['id'] == active_id.to_i) ? " (active)" : ""),
        #     name: security_group['name'],
        #     description: security_group['description']
        #   }
        # end

        # columns = [
        #   :id,
        #   :name,
        #   :description,
        #   # :ports,
        #   # :status,
        # ]
        columns = {
          "ID" => 'id',
          "NAME" => 'name',
          #"DESCRIPTION" => 'description',
          "DESCRIPTION" => lambda {|it| truncate_string(it['description'], 30) },
          #"USED BY" => lambda {|it| it['associations'] ? it['associations'] : '' },
          "SCOPED CLOUD" => lambda {|it| it['zone'] ? it['zone']['name'] : 'All' },
          "SOURCE" => lambda {|it| it['syncSource'] == 'external' ? 'SYNCED' : 'CREATED' }
        }
        # custom pretty table columns ...
        if options[:include_fields]
          columns = options[:include_fields]
        end
        print as_pretty_table(security_groups, columns, options)
        print reset
        if json_response['meta']
          print_results_pagination(json_response)
        else
          print_results_pagination({'meta'=>{'total'=>(json_response['securityGroupCount'] ? json_response['securityGroupCount'] : security_groups.size),'size'=>security_groups.size,'max'=>(params['max']||25),'offset'=>(params['offset']||0)}})
        end
        # print reset
      end
      print reset,"\n"
      return 0
    rescue RestClient::Exception => e
      print_rest_exception(e, options)
      exit 1
    end
  end

  def get(args)
    options = {}
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[id]")
      build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
      opts.footer = "Get details about a security group."
    end
    optparse.parse!(args)
    if args.count != 1
      raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    connect(options)
    exit_code, err = 0, nil
    
      security_group_id = nil
      if args[0].to_s =~ /\A\d{1,}\Z/
        security_group_id = args[0].to_i
      else
        security_group = find_security_group_by_name(args[0])
        return 1, "Security Group not found" if security_group.nil?
        security_group_id = security_group['id']
      end
      @security_groups_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @security_groups_interface.dry.get(security_group_id)
        return exit_code, err
      end
      json_response = @security_groups_interface.get(security_group_id)
      render_result = render_with_format(json_response, options, 'securityGroup')
      return exit_code, err if render_result

      security_group = json_response['securityGroup']
      security_group_locations = json_response['locations'] || security_group['locations']
      security_group_rules = json_response['rules'] || security_group['rules']

      print_h1 "Security Group Details"
      print cyan
      description_cols = {
        "ID" => 'id',
        "Name" => 'name',
        "Description" => 'description',
        "Scoped Cloud" => lambda {|it| it['zone'] ? it['zone']['name'] : 'All' },
        "Source" => lambda {|it| it['syncSource'] == 'external' ? 'SYNCED' : 'CREATED' },
        # "Active" => lambda {|it| format_boolean(it['active']) },
        "Visibility" => 'visibility',
        "Tenants" => lambda {|it| it['tenants'] ? it['tenants'].collect {|it| it['name'] }.uniq.sort.join(', ') : '' },
      }
      print_description_list(description_cols, security_group)
      # print reset,"\n"

      if security_group['resourcePermission'].nil?
        #print "\n", "No group access found", "\n"
      else
        print_h2 "Group Access"
        rows = []
        if security_group['resourcePermission']['all']
          rows.push({"name" => 'All'})
        end
        if security_group['resourcePermission']['sites']
          security_group['resourcePermission']['sites'].each do |site|
            rows.push(site)
          end
        end
        rows = rows.collect do |site|
          {group: site['name'], default: site['default'] ? 'Yes' : ''}
        end
        columns = [:group, :default]
        print cyan
        print as_pretty_table(rows, columns)
      end

      if security_group_locations && security_group_locations.size > 0
        print_h2 "Locations"
        print cyan
        location_cols = {
          "ID" => 'id',
          "CLOUD" => lambda {|it| it['zone'] ? it['zone']['name'] : '' },
          "EXTERNAL ID" => lambda {|it| it['externalId'] },
          "RESOURCE POOL" => lambda {|it| it['zonePool'] ? it['zonePool']['name'] : '' }
        }
        puts as_pretty_table(security_group_locations, location_cols)
      else
        print reset,"\n"
      end

      if security_group_rules
        if security_group_rules == 0
          #print cyan,"No rules.",reset,"\n"
        else
          print_h2 "Rules"
          print cyan
          # NAME  DIRECTION SOURCE  DESTINATION RULE TYPE PROTOCOL  PORT RANGE
          rule_cols = {
            "ID" => 'id',
            "NAME" => 'name',
            "DIRECTION" => lambda {|it| it['direction'] },
            "SOURCE" => lambda {|it| 
              if it['sourceType'] == 'cidr'
                "Network: #{it['source']}"
              elsif it['sourceType'] == 'group'
                "Group: #{it['sourceGroup'] ? it['sourceGroup']['name'] : ''}"
              elsif it['sourceType'] == 'tier'
                "Tier: #{it['sourceTier'] ? it['sourceTier']['name'] : ''}"
              elsif it['sourceType'] == 'instance'
                "Instance"
              else
                it['sourceType']
              end
            },
            "DESTINATION" => lambda {|it| 
              if it['destinationType'] == 'cidr'
                "Network: #{it['destination']}"
              elsif it['destinationType'] == 'group'
                "Group: #{it['destinationGroup'] ? it['destinationGroup']['name'] : ''}"
              elsif it['destinationType'] == 'tier'
                "Tier: #{it['destinationTier'] ? it['destinationTier']['name'] : ''}"
              elsif it['destinationType'] == 'instance'
                "Instance"
              else
                it['destinationType']
              end
            },
            "RULE TYPE" => lambda {|it| it['ruleType'] == 'customRule' ? 'Custom' : it['ruleType'] },
            "PROTOCOL" => lambda {|it| it['protocol'] },
            "PORT RANGE" => lambda {|it| it['portRange'] }
          }
          puts as_pretty_table(security_group_rules, rule_cols)
        end
      end

      return exit_code, err
  end

  def add(args)
    params = {}
    options = {:options => {}}
    cloud_id = nil
    tenants = nil
    group_access_all = nil
    group_access_list = nil
    group_defaults_list = nil
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[name] [options]")
      opts.on( '--name Name', String, "Name of the security group" ) do |val|
        options[:options]['name'] = val
      end
      opts.on( '--description Description', String, "Description of the security group" ) do |val|
        options[:options]['description'] = val
      end
      opts.on( '-c', '--cloud CLOUD', "Scoped Cloud Name or ID" ) do |val|
        cloud_id = val
      end
      opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
        group_access_all = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
      end
      opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_access_list = []
        else
          group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--group-defaults LIST', Array, "Group Default Selection, comma separated list of group IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_defaults_list = []
        else
          group_defaults_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          options['tenants'] = []
        else
          options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--can-manage LIST', Array, "Tenant Can Manage, comma separated list of account IDs that can manage") do |list|
        options['canManage'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
      end
      opts.on('--visibility [private|public]', String, "Visibility") do |val|
        options['visibility'] = val
      end
      build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
      opts.footer = "Create a security group." + "\n" +
                    "[name] is required. This is the name of the security group."
    end
    optparse.parse!(args)
    if args.count > 1
      raise_command_error "wrong number of arguments, expected 0-1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    if args[0]
      options[:options]['name'] = args[0]
    end
    connect(options)
    begin

      # load cloud
      cloud = nil
      if cloud_id
        cloud = find_cloud_by_name_or_id(cloud_id)
        return 1 if cloud.nil?
        options[:options]['zoneId'] = cloud['id'].to_s
      end

      # 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!({'securityGroup' => passed_options})  unless passed_options.empty?
      else
        # prompt for resource folder options
        payload = {
          'securityGroup' => {
          }
        }
        # allow arbitrary -O options
        payload.deep_merge!({'securityGroup' => passed_options})  unless passed_options.empty?

        # Name
        options[:options]['name'] = options[:name] if options.key?(:name)
        v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true}], options[:options])
        payload['securityGroup']['name'] = v_prompt['name']

        # Description
        v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false}], options[:options])
        payload['securityGroup']['description'] = v_prompt['description']

        # Scoped Cloud
        # /api/options/clouds requires groupId...
        
        
        begin
          scoped_clouds = [{"name" => "All", "value" => "all"}]
          clouds_response = @options_interface.options_for_source('cloudsForSecurityGroup',{})
          if clouds_response['data']
            clouds_response['data'].each do |it|
              scoped_clouds << {"name" => it['name'], "value" => it['value']}
            end
          end
          v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'zoneId', 'fieldLabel' => 'Scoped Cloud', 'type' => 'select', 'selectOptions' => scoped_clouds, 'required' => false, 'defaultValue' => (payload['securityGroup']['zoneId'] || 'all')}], options[:options], @api_client)
          #v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'zoneId', 'fieldLabel' => 'Scoped Cloud', 'type' => 'select', 'optionSource' => 'cloudsForSecurityGroup', 'required' => false}], options[:options], @api_client)
          if !v_prompt['zoneId'].to_s.empty? && v_prompt['zoneId'].to_s != 'all' && v_prompt['zoneId'].to_s != '-1'
            payload['securityGroup']['zoneId'] = v_prompt['zoneId']

            zone = find_cloud_by_id(payload['securityGroup']['zoneId'])
            if zone['securityServer']
              sec_server = @network_security_servers.get(zone['securityServer']['id'])['networkSecurityServer']

              if sec_server['type']
                payload['securityGroup'].deep_merge!(Morpheus::Cli::OptionTypes.prompt(sec_server['type']['optionTypes'], options[:options], @api_client, {zoneId: zone['id']}))
              end
            end
          end
        rescue => ex
          print yellow,"Failed to determine the available scoped clouds.",reset,"\n"
        end

        # Group Access
        if group_access_all != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['all'] = group_access_all
        end
        if group_access_list != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
            site = {"id" => site_id.to_i}
            if group_defaults_list && group_defaults_list.include?(site_id)
              site["default"] = true
            end
            site
          end
        end

        # Tenants
        if options['tenants']
          payload['tenantPermissions'] = {}
          payload['tenantPermissions']['accounts'] = options['tenants']
          payload['tenantPermissions']['canManageAccounts'] = options['canManage'] if options['canManage']
        end

        # Visibility
        if options['visibility'] != nil
          payload['securityGroup']['visibility'] = options['visibility']
        end

        # Active
        if options['active'] != nil
          payload['securityGroup']['active'] = options['active']
        end

      end

      @security_groups_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @security_groups_interface.dry.create(payload)
        return 0
      end
      json_response = @security_groups_interface.create(payload)
      if options[:json]
        puts as_json(json_response, options)
        return 0
      end
      print_green_success "Created security group #{json_response['securityGroup']['name']}"
      return 0
    rescue RestClient::Exception => e
      print_rest_exception(e, options)
      exit 1
    end
  end

  def update(args)
    params = {}
    options = {:options => {}}
    tenants = nil
    group_access_all = nil
    group_access_list = nil
    group_defaults_list = nil
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[security-group] [options]")
      opts.on( '--name Name', String, "Name of the security group" ) do |val|
        options[:options]['name'] = val
      end
      opts.on( '--description Description', String, "Description of the security group" ) do |val|
        options[:options]['description'] = val
      end
      opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
        group_access_all = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
      end
      opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_access_list = []
        else
          group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--group-defaults LIST', Array, "Group Default Selection, comma separated list of group IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_defaults_list = []
        else
          group_defaults_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          options['tenants'] = []
        else
          options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--can-manage LIST', Array, "Tenant Can Manage, comma separated list of account IDs that can manage") do |list|
        options['canManage'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
      end
      opts.on('--visibility [private|public]', String, "Visibility") do |val|
        options['visibility'] = val
      end
      build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
      opts.footer = "Update a security group." + "\n" +
                    "[security-group] is required. This is the name or id of the security group."
    end
    optparse.parse!(args)
    if args.count != 1
      raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    connect(options)
    begin
      security_group = find_security_group_by_name_or_id(args[0])
      return 1 if security_group.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['securityGroup'].deep_merge!(passed_options)  unless passed_options.empty?
      else
        # prompt for resource folder options
        payload = {
          'securityGroup' => {
          }
        }
        # allow arbitrary -O options
        payload['securityGroup'].deep_merge!(passed_options)  unless passed_options.empty?
        
        # Group Access
        if group_access_all != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['all'] = group_access_all
        end
        if group_access_list != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
            site = {"id" => site_id.to_i}
            if group_defaults_list && group_defaults_list.include?(site_id)
              site["default"] = true
            end
            site
          end
        end

        # Tenants
        if options['tenants']
          payload['tenantPermissions'] = {}
          payload['tenantPermissions']['accounts'] = options['tenants']
          payload['tenantPermissions']['canManageAccounts'] = options['canManage'] if options['canManage']
        end

        # Visibility
        if options['visibility'] != nil
          payload['securityGroup']['visibility'] = options['visibility']
        end

        # Active
        if options['active'] != nil<commit_msg>changed security groups add / update command to imply tenant access when in can manage list<commit_after># require 'yaml'
require 'io/console'
require 'rest_client'
require 'optparse'
require 'filesize'
require 'morpheus/cli/cli_command'
require 'morpheus/cli/mixins/infrastructure_helper'

class Morpheus::Cli::SecurityGroups
  include Morpheus::Cli::CliCommand
  include Morpheus::Cli::InfrastructureHelper

  register_subcommands :list, :get, :add, :update, :remove
  register_subcommands :'add-location', :'remove-location'
  register_subcommands :'add-rule', :'update-rule', :'remove-rule'
  set_default_subcommand :list
  
  def connect(opts)
    @api_client = establish_remote_appliance_connection(opts)
    @security_groups_interface = @api_client.security_groups
    @security_group_rules_interface = @api_client.security_group_rules
    @cloud_resource_pools_interface = @api_client.cloud_resource_pools
    @clouds_interface = @api_client.clouds
    @options_interface = @api_client.options
    @active_security_group = ::Morpheus::Cli::SecurityGroups.load_security_group_file
    @network_security_servers = @api_client.network_security_servers
  end

  def handle(args)
    handle_subcommand(args)
  end


  def list(args)
    options = {}
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage()
      build_common_options(opts, options, [:list, :query, :json, :yaml, :csv, :fields, :dry_run, :remote])
      opts.footer = "List security groups."
    end
    optparse.parse!(args)
    if args.count != 0
      raise_command_error "wrong number of arguments, expected 0 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    connect(options)
    begin
      params = {}
      params.merge!(parse_list_options(options))
      @security_groups_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @security_groups_interface.dry.list(params)
        return
      end
      json_response = @security_groups_interface.list(params)
      
      render_result = render_with_format(json_response, options, 'securityGroups')
      return 0 if render_result

      title = "Morpheus Security Groups"
      subtitles = []
      subtitles += parse_list_subtitles(options)
      print_h1 title, subtitles

      security_groups = json_response['securityGroups']
      
      if security_groups.empty?
        print yellow,"No security groups found.",reset,"\n"
      else
        active_id = @active_security_group[@appliance_name.to_sym]
        # table_color = options[:color] || cyan
        # rows = security_groups.collect do |security_group|
        #   {
        #     id: security_group['id'].to_s + ((security_group['id'] == active_id.to_i) ? " (active)" : ""),
        #     name: security_group['name'],
        #     description: security_group['description']
        #   }
        # end

        # columns = [
        #   :id,
        #   :name,
        #   :description,
        #   # :ports,
        #   # :status,
        # ]
        columns = {
          "ID" => 'id',
          "NAME" => 'name',
          #"DESCRIPTION" => 'description',
          "DESCRIPTION" => lambda {|it| truncate_string(it['description'], 30) },
          #"USED BY" => lambda {|it| it['associations'] ? it['associations'] : '' },
          "SCOPED CLOUD" => lambda {|it| it['zone'] ? it['zone']['name'] : 'All' },
          "SOURCE" => lambda {|it| it['syncSource'] == 'external' ? 'SYNCED' : 'CREATED' }
        }
        # custom pretty table columns ...
        if options[:include_fields]
          columns = options[:include_fields]
        end
        print as_pretty_table(security_groups, columns, options)
        print reset
        if json_response['meta']
          print_results_pagination(json_response)
        else
          print_results_pagination({'meta'=>{'total'=>(json_response['securityGroupCount'] ? json_response['securityGroupCount'] : security_groups.size),'size'=>security_groups.size,'max'=>(params['max']||25),'offset'=>(params['offset']||0)}})
        end
        # print reset
      end
      print reset,"\n"
      return 0
    rescue RestClient::Exception => e
      print_rest_exception(e, options)
      exit 1
    end
  end

  def get(args)
    options = {}
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[id]")
      build_common_options(opts, options, [:json, :yaml, :csv, :fields, :dry_run, :remote])
      opts.footer = "Get details about a security group."
    end
    optparse.parse!(args)
    if args.count != 1
      raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    connect(options)
    exit_code, err = 0, nil
    
      security_group_id = nil
      if args[0].to_s =~ /\A\d{1,}\Z/
        security_group_id = args[0].to_i
      else
        security_group = find_security_group_by_name(args[0])
        return 1, "Security Group not found" if security_group.nil?
        security_group_id = security_group['id']
      end
      @security_groups_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @security_groups_interface.dry.get(security_group_id)
        return exit_code, err
      end
      json_response = @security_groups_interface.get(security_group_id)
      render_result = render_with_format(json_response, options, 'securityGroup')
      return exit_code, err if render_result

      security_group = json_response['securityGroup']
      security_group_locations = json_response['locations'] || security_group['locations']
      security_group_rules = json_response['rules'] || security_group['rules']

      print_h1 "Security Group Details"
      print cyan
      description_cols = {
        "ID" => 'id',
        "Name" => 'name',
        "Description" => 'description',
        "Scoped Cloud" => lambda {|it| it['zone'] ? it['zone']['name'] : 'All' },
        "Source" => lambda {|it| it['syncSource'] == 'external' ? 'SYNCED' : 'CREATED' },
        # "Active" => lambda {|it| format_boolean(it['active']) },
        "Visibility" => 'visibility',
        "Tenants" => lambda {|it| it['tenants'] ? it['tenants'].collect {|it| it['name'] }.uniq.sort.join(', ') : '' },
      }
      print_description_list(description_cols, security_group)
      # print reset,"\n"

      if security_group['resourcePermission'].nil?
        #print "\n", "No group access found", "\n"
      else
        print_h2 "Group Access"
        rows = []
        if security_group['resourcePermission']['all']
          rows.push({"name" => 'All'})
        end
        if security_group['resourcePermission']['sites']
          security_group['resourcePermission']['sites'].each do |site|
            rows.push(site)
          end
        end
        rows = rows.collect do |site|
          {group: site['name'], default: site['default'] ? 'Yes' : ''}
        end
        columns = [:group, :default]
        print cyan
        print as_pretty_table(rows, columns)
      end

      if security_group_locations && security_group_locations.size > 0
        print_h2 "Locations"
        print cyan
        location_cols = {
          "ID" => 'id',
          "CLOUD" => lambda {|it| it['zone'] ? it['zone']['name'] : '' },
          "EXTERNAL ID" => lambda {|it| it['externalId'] },
          "RESOURCE POOL" => lambda {|it| it['zonePool'] ? it['zonePool']['name'] : '' }
        }
        puts as_pretty_table(security_group_locations, location_cols)
      else
        print reset,"\n"
      end

      if security_group_rules
        if security_group_rules == 0
          #print cyan,"No rules.",reset,"\n"
        else
          print_h2 "Rules"
          print cyan
          # NAME  DIRECTION SOURCE  DESTINATION RULE TYPE PROTOCOL  PORT RANGE
          rule_cols = {
            "ID" => 'id',
            "NAME" => 'name',
            "DIRECTION" => lambda {|it| it['direction'] },
            "SOURCE" => lambda {|it| 
              if it['sourceType'] == 'cidr'
                "Network: #{it['source']}"
              elsif it['sourceType'] == 'group'
                "Group: #{it['sourceGroup'] ? it['sourceGroup']['name'] : ''}"
              elsif it['sourceType'] == 'tier'
                "Tier: #{it['sourceTier'] ? it['sourceTier']['name'] : ''}"
              elsif it['sourceType'] == 'instance'
                "Instance"
              else
                it['sourceType']
              end
            },
            "DESTINATION" => lambda {|it| 
              if it['destinationType'] == 'cidr'
                "Network: #{it['destination']}"
              elsif it['destinationType'] == 'group'
                "Group: #{it['destinationGroup'] ? it['destinationGroup']['name'] : ''}"
              elsif it['destinationType'] == 'tier'
                "Tier: #{it['destinationTier'] ? it['destinationTier']['name'] : ''}"
              elsif it['destinationType'] == 'instance'
                "Instance"
              else
                it['destinationType']
              end
            },
            "RULE TYPE" => lambda {|it| it['ruleType'] == 'customRule' ? 'Custom' : it['ruleType'] },
            "PROTOCOL" => lambda {|it| it['protocol'] },
            "PORT RANGE" => lambda {|it| it['portRange'] }
          }
          puts as_pretty_table(security_group_rules, rule_cols)
        end
      end

      return exit_code, err
  end

  def add(args)
    params = {}
    options = {:options => {}}
    cloud_id = nil
    tenants = nil
    group_access_all = nil
    group_access_list = nil
    group_defaults_list = nil
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[name] [options]")
      opts.on( '--name Name', String, "Name of the security group" ) do |val|
        options[:options]['name'] = val
      end
      opts.on( '--description Description', String, "Description of the security group" ) do |val|
        options[:options]['description'] = val
      end
      opts.on( '-c', '--cloud CLOUD', "Scoped Cloud Name or ID" ) do |val|
        cloud_id = val
      end
      opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
        group_access_all = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
      end
      opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_access_list = []
        else
          group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--group-defaults LIST', Array, "Group Default Selection, comma separated list of group IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_defaults_list = []
        else
          group_defaults_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          options['tenants'] = []
        else
          options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--can-manage LIST', Array, "Tenant Can Manage, comma separated list of account IDs that can manage") do |list|
        options['canManage'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
      end
      opts.on('--visibility [private|public]', String, "Visibility") do |val|
        options['visibility'] = val
      end
      build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
      opts.footer = "Create a security group." + "\n" +
                    "[name] is required. This is the name of the security group."
    end
    optparse.parse!(args)
    if args.count > 1
      raise_command_error "wrong number of arguments, expected 0-1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    if args[0]
      options[:options]['name'] = args[0]
    end
    connect(options)
    begin

      # load cloud
      cloud = nil
      if cloud_id
        cloud = find_cloud_by_name_or_id(cloud_id)
        return 1 if cloud.nil?
        options[:options]['zoneId'] = cloud['id'].to_s
      end

      # 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!({'securityGroup' => passed_options})  unless passed_options.empty?
      else
        # prompt for resource folder options
        payload = {
          'securityGroup' => {
          }
        }
        # allow arbitrary -O options
        payload.deep_merge!({'securityGroup' => passed_options})  unless passed_options.empty?

        # Name
        options[:options]['name'] = options[:name] if options.key?(:name)
        v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'name', 'fieldLabel' => 'Name', 'type' => 'text', 'required' => true}], options[:options])
        payload['securityGroup']['name'] = v_prompt['name']

        # Description
        v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'description', 'fieldLabel' => 'Description', 'type' => 'text', 'required' => false}], options[:options])
        payload['securityGroup']['description'] = v_prompt['description']

        # Scoped Cloud
        # /api/options/clouds requires groupId...
        
        
        begin
          scoped_clouds = [{"name" => "All", "value" => "all"}]
          clouds_response = @options_interface.options_for_source('cloudsForSecurityGroup',{})
          if clouds_response['data']
            clouds_response['data'].each do |it|
              scoped_clouds << {"name" => it['name'], "value" => it['value']}
            end
          end
          v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'zoneId', 'fieldLabel' => 'Scoped Cloud', 'type' => 'select', 'selectOptions' => scoped_clouds, 'required' => false, 'defaultValue' => (payload['securityGroup']['zoneId'] || 'all')}], options[:options], @api_client)
          #v_prompt = Morpheus::Cli::OptionTypes.prompt([{'fieldName' => 'zoneId', 'fieldLabel' => 'Scoped Cloud', 'type' => 'select', 'optionSource' => 'cloudsForSecurityGroup', 'required' => false}], options[:options], @api_client)
          if !v_prompt['zoneId'].to_s.empty? && v_prompt['zoneId'].to_s != 'all' && v_prompt['zoneId'].to_s != '-1'
            payload['securityGroup']['zoneId'] = v_prompt['zoneId']

            zone = find_cloud_by_id(payload['securityGroup']['zoneId'])
            if zone['securityServer']
              sec_server = @network_security_servers.get(zone['securityServer']['id'])['networkSecurityServer']

              if sec_server['type']
                payload['securityGroup'].deep_merge!(Morpheus::Cli::OptionTypes.prompt(sec_server['type']['optionTypes'], options[:options], @api_client, {zoneId: zone['id']}))
              end
            end
          end
        rescue => ex
          print yellow,"Failed to determine the available scoped clouds.",reset,"\n"
        end

        # Group Access
        if group_access_all != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['all'] = group_access_all
        end
        if group_access_list != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
            site = {"id" => site_id.to_i}
            if group_defaults_list && group_defaults_list.include?(site_id)
              site["default"] = true
            end
            site
          end
        end

        # Tenants
        if options['tenants'] || options['canManage']
          payload['tenantPermissions'] = {}
          payload['tenantPermissions']['accounts'] = ((options['tenants'] || []) + (options['canManage'] || [])).uniq
          payload['tenantPermissions']['canManageAccounts'] = options['canManage'] if options['canManage']
        end

        # Visibility
        if options['visibility'] != nil
          payload['securityGroup']['visibility'] = options['visibility']
        end

        # Active
        if options['active'] != nil
          payload['securityGroup']['active'] = options['active']
        end

      end

      @security_groups_interface.setopts(options)
      if options[:dry_run]
        print_dry_run @security_groups_interface.dry.create(payload)
        return 0
      end
      json_response = @security_groups_interface.create(payload)
      if options[:json]
        puts as_json(json_response, options)
        return 0
      end
      print_green_success "Created security group #{json_response['securityGroup']['name']}"
      return 0
    rescue RestClient::Exception => e
      print_rest_exception(e, options)
      exit 1
    end
  end

  def update(args)
    params = {}
    options = {:options => {}}
    tenants = nil
    group_access_all = nil
    group_access_list = nil
    group_defaults_list = nil
    optparse = Morpheus::Cli::OptionParser.new do |opts|
      opts.banner = subcommand_usage("[security-group] [options]")
      opts.on( '--name Name', String, "Name of the security group" ) do |val|
        options[:options]['name'] = val
      end
      opts.on( '--description Description', String, "Description of the security group" ) do |val|
        options[:options]['description'] = val
      end
      opts.on('--group-access-all [on|off]', String, "Toggle Access for all groups.") do |val|
        group_access_all = val.to_s == 'on' || val.to_s == 'true' || val.to_s == ''
      end
      opts.on('--group-access LIST', Array, "Group Access, comma separated list of group IDs.") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_access_list = []
        else
          group_access_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--group-defaults LIST', Array, "Group Default Selection, comma separated list of group IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          group_defaults_list = []
        else
          group_defaults_list = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--tenants LIST', Array, "Tenant Access, comma separated list of account IDs") do |list|
        if list.size == 1 && list[0] == 'null' # hacky way to clear it
          options['tenants'] = []
        else
          options['tenants'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
        end
      end
      opts.on('--can-manage LIST', Array, "Tenant Can Manage, comma separated list of account IDs that can manage") do |list|
        options['canManage'] = list.collect {|it| it.to_s.strip.empty? ? nil : it.to_s.strip }.compact.uniq
      end
      opts.on('--visibility [private|public]', String, "Visibility") do |val|
        options['visibility'] = val
      end
      build_common_options(opts, options, [:options, :payload, :json, :dry_run, :remote])
      opts.footer = "Update a security group." + "\n" +
                    "[security-group] is required. This is the name or id of the security group."
    end
    optparse.parse!(args)
    if args.count != 1
      raise_command_error "wrong number of arguments, expected 1 and got (#{args.count}) #{args.join(' ')}\n#{optparse}"
    end
    connect(options)
    begin
      security_group = find_security_group_by_name_or_id(args[0])
      return 1 if security_group.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['securityGroup'].deep_merge!(passed_options)  unless passed_options.empty?
      else
        # prompt for resource folder options
        payload = {
          'securityGroup' => {
          }
        }
        # allow arbitrary -O options
        payload['securityGroup'].deep_merge!(passed_options)  unless passed_options.empty?
        
        # Group Access
        if group_access_all != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['all'] = group_access_all
        end
        if group_access_list != nil
          payload['resourcePermissions'] ||= {}
          payload['resourcePermissions']['sites'] = group_access_list.collect do |site_id|
            site = {"id" => site_id.to_i}
            if group_defaults_list && group_defaults_list.include?(site_id)
              site["default"] = true
            end
            site
          end
        end

        # Tenants
        if options['tenants'] || options['canManage']
          payload['tenantPermissions'] = {}
          payload['tenantPermissions']['accounts'] = ((options['tenants'] || []) + (options['canManage'] || [])).uniq
          payload['tenantPermissions']['canManageAccounts'] = options['canManage'] if options['canManage']
        end

        # Visibility
        if options['visibility'] != nil
          payload['securityGroup']['visibility'] = options['visibility']
        end

        # Active
        if options['active'] != nil
{'fraction_non_alphanumeric': 0.1419167248197255, 'fraction_numerical': 0.002419167248197255, 'mean_word_length': 2.5668298348958767, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 141, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '505712', 'n_tokens_mistral': 12934, 'n_tokens_neox': 12609, 'n_words': 2879}