bin/trema

Summary

Maintainability
Test Coverage
#!/usr/bin/env ruby
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')

require 'drb/drb'
require 'gli'
require 'phut'
require 'pio'
require 'trema'

# OpenFlow controller framework.
module Trema
  # trema command.
  # rubocop:disable ModuleLength
  module App
    extend GLI::App

    desc 'Displays the current runtime version'
    program_desc 'Trema command-line tool'

    version Trema::VERSION

    desc 'Be verbose'
    switch [:v, :verbose], negatable: false

    desc 'Runs a trema application'
    arg_name 'controller'
    command :run do |c|
      c.desc 'Runs as a daemon'
      c.switch [:d, :daemonize], negatable: false
      c.desc 'Specifies emulated network configuration'
      c.flag [:c, :conf]
      c.desc 'Use OpenFlow1.3'
      c.switch :openflow13, default_value: false

      c.desc 'Overrides the default openflow channel port'
      c.flag [:p, :port]

      c.desc 'Set logging level'
      c.flag [:l, :logging_level], default_value: :info

      c.desc 'Location to put pid files'
      c.flag [:P, :pid_dir], default_value: Trema::DEFAULT_PID_DIR
      c.desc 'Location to put log files'
      c.flag [:L, :log_dir], default_value: Trema::DEFAULT_LOG_DIR
      c.desc 'Location to put socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |global_options, options, args|
        Phut.pid_dir = options[:pid_dir]
        Phut.log_dir = options[:log_dir]
        Phut.socket_dir = options[:socket_dir]
        Pio::OpenFlow.switch_version('OpenFlow13') if options[:openflow13]
        begin
          options[:logging_level] =
            { debug: ::Logger::DEBUG,
              info: ::Logger::INFO,
              warn: ::Logger::WARN,
              error: ::Logger::ERROR,
              fatal: ::Logger::FATAL,
              unknown: ::Logger::UNKNOWN
            }.fetch(options[:logging_level].to_sym)
          options[:logging_level] = ::Logger::DEBUG if global_options[:verbose]
        rescue KeyError
          raise(ArgumentError,
                "Invalid log level: #{options[:logging_level]}")
        end
        require 'trema/switch'
        Trema::Command.new.run(args, global_options.merge(options))
      end
    end

    desc 'Print all flow entries'
    arg_name 'switch'
    command :dump_flows do |c|
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, args|
        puts Trema.fetch(args.first, options.fetch(:socket_dir)).dump_flows
      end
    end

    desc 'Sends UDP packets to destination host'
    command :send_packets do |c|
      c.desc 'host that sends packets'
      c.flag [:s, :source]
      c.desc 'host that receives packets'
      c.flag [:d, :dest]
      c.desc 'number of packets to send'
      c.flag [:n, :npackets], default_value: 1

      c.desc 'Location to put socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, _args|
        raise '--source option is mandatory' if options[:source].nil?
        raise '--dest option is mandatory' if options[:dest].nil?
        dest = Trema.fetch(options.fetch(:dest), options.fetch(:socket_dir))
        Phut::VhostDaemon.
          process(options.fetch(:source), options.fetch(:socket_dir)).
          send_packets(dest, options.fetch(:npackets).to_i)
      end
    end

    desc 'Shows stats of packets'
    arg_name 'host'
    command :show_stats do |c|
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, args|
        help_now!('host is required') if args.empty?

        stats = Phut::VhostDaemon.process(args[0], options[:socket_dir]).stats

        dests = stats[:tx].map { |each| each.destination_ip_address.to_s }.uniq
        txstats = dests.map do |each|
          all = stats[:tx].select { |pkt| pkt.destination_ip_address == each }
          "#{all.first.source_ip_address} -> #{each} = " \
          "#{all.size} packet#{all.size > 1 ? 's' : ''}"
        end
        unless txstats.empty?
          puts 'Packets sent:'
          txstats.each { |each| puts "  #{each}" }
        end

        sources = stats[:rx].map { |each| each.source_ip_address.to_s }.uniq
        rxstats = sources.map do |each|
          all = stats[:rx].select { |pkt| pkt.source_ip_address == each }
          "#{each} -> #{all.first.destination_ip_address} = " \
          "#{all.size} packet#{all.size > 1 ? 's' : ''}"
        end
        unless rxstats.empty?
          puts 'Packets received:'
          rxstats.each { |each| puts "  #{each}" }
        end
      end
    end

    desc 'Reset stats of packets'
    command :reset_stats do |c|
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, _args|
        Trema.vhosts(options[:socket_dir]).each(&:reset_stats)
      end
    end

    desc "Brings a switch's specified port up"
    command :port_up do |c|
      c.desc 'switch name'
      c.flag [:s, :switch]
      c.desc 'port'
      c.flag [:p, :port]
      c.desc 'Location to put socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, _args|
        raise '--switch option is mandatory' if options[:switch].nil?
        raise '--port option is mandatory' if options[:port].nil?
        Trema.trema_processes(options[:socket_dir]).each do |trema|
          begin
            trema.port_up(options[:switch], options[:port].to_i)
          rescue
            next
          end
        end
      end
    end

    desc "Brings a switch's specified port down"
    command :port_down do |c|
      c.desc 'switch name'
      c.flag [:s, :switch]
      c.desc 'port'
      c.flag [:p, :port]
      c.desc 'Location to put socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, _args|
        raise '--switch option is mandatory' if options[:switch].nil?
        raise '--port option is mandatory' if options[:port].nil?
        Trema.trema_processes(options[:socket_dir]).each do |trema|
          begin
            trema.port_down(options[:switch], options[:port].to_i)
          rescue
            next
          end
        end
      end
    end

    desc 'Stops a vswitch or a vhost'
    arg_name 'name'
    command :stop do |c|
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, args|
        help_now! if args.size != 1
        Trema.fetch(args[0], options[:socket_dir]).stop
      end
    end

    desc 'Deletes a virtual link'
    arg_name 'endpoint1 endpoint2'
    command :delete_link do |c|
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, args|
        help_now! if args.size != 2
        Trema.fetch(args, options[:socket_dir]).stop
      end
    end

    desc 'Starts the stopped vswitch or vhost again'
    arg_name 'name'
    command :start do |c|
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, args|
        help_now! if args.size != 1
        Trema.fetch(args[0], options[:socket_dir]).run
      end
    end

    desc 'Terminates all trema processes'
    arg_name 'controller_name'
    command :killall do |c|
      c.desc 'Kill all known trema processes'
      c.switch :all, default_value: false, negatable: false
      c.desc 'Location to find socket files'
      c.flag [:S, :socket_dir], default_value: Trema::DEFAULT_SOCKET_DIR

      c.action do |_global_options, options, args|
        if options[:all]
          Trema.trema_processes(options[:socket_dir]).each do |each|
            begin
              each.killall
            rescue DRb::DRbConnError
              true # OK (trema process exitted).
            end
          end
        else
          help_now! if args.size != 1
          begin
            Trema.trema_process(args[0], options[:socket_dir]).killall
          rescue DRb::DRbConnError
            true # OK (trema process exitted).
          end
        end
      end
    end

    # rubocop:disable LineLength
    desc 'Opens a new shell or runs a command in the specified network namespace'
    arg_name 'name [command]'
    command :netns do |c|
      c.action do |_global_options, _options, args|
        command_args = args[1..-1]
        if command_args && !command_args.empty?
          system "sudo ip netns exec #{args[0]} #{command_args.join(' ')}"
        else
          system "sudo ip netns exec #{args[0]} #{ENV['SHELL']}"
        end
      end
    end
    # rubocop:enable LineLength

    default_command :help

    on_error do |e|
      case e
      when OptionParser::ParseError,
           Trema::NoControllerDefined,
           Phut::OpenVswitch::AlreadyRunning,
           GLI::UnknownCommandArgument,
           ArgumentError
        true
      when Interrupt
        exit false
      else
        # show backtrace
        raise e
      end
    end

    exit run(ARGV)
  end
  # rubocop:enable ModuleLength
end