#!/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