diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..560d1a6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +*.gem +*.rbc +.bundle +.config +coverage +InstalledFiles +lib/bundler/man +pkg +rdoc +spec/reports +test/tmp +test/version_tmp +tmp + +# YARD artifacts +.yardoc +_yardoc +doc/ diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..56cbf82 --- /dev/null +++ b/Gemfile @@ -0,0 +1,4 @@ +source 'https://rubygems.org' + +# Specify your gem's dependencies in omf_rc_foo.gemspec +gemspec diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..32574e6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2012 Jack C Hong + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a62eb3c --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# OmfRcOpenflow + +OMF Resource Controllers related to Openflow + +## Installation + +Add this line to your application's Gemfile: + + gem 'omf_rc_openflow' + +And then execute: + + $ bundle + +Or install it yourself as: + + $ gem install omf_rc_openflow + +## Usage + +TODO: Write usage instructions here + +## Contributing + +1. Fork it +2. Create your feature branch (`git checkout -b my-new-feature`) +3. Commit your changes (`git commit -am 'Added some feature'`) +4. Push to the branch (`git push origin my-new-feature`) +5. Create new Pull Request diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f57ae68 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +#!/usr/bin/env rake +require "bundler/gem_tasks" diff --git a/bin/openflow_scenario b/bin/openflow_scenario new file mode 100755 index 0000000..adf666a --- /dev/null +++ b/bin/openflow_scenario @@ -0,0 +1,142 @@ +#!/usr/bin/env ruby + +require 'omf_common' +$stdout.sync = true + +include OmfCommon + +options = { + user: 'user', + password: 'pw', + server: 'srv.mytestbed.net', # XMPP pubsub server domain + uid: 'flowvisor', + debug: false +} + +flowvisor_id = options[:uid] + +Logging.logger.root.level = options[:debug] ? :debug : :info +Blather.logger = logger + +# We will use Comm directly, with default DSL implementaion :xmpp +comm = Comm.new(:xmpp) + +flowvisor_topic = comm.get_topic(flowvisor_id) + +port2ip = { + 16 => '10.0.1.18', + 21 => '10.0.1.19', + 23 => '10.0.1.20', + 15 => '10.0.1.21' +} + +port2eth = { + 16 => '00:03:2d:0d:30:c0', + 21 => '00:03:2d:0d:30:d4', + 23 => '00:03:2d:0d:30:cc', + 15 => '00:03:2d:0d:30:ce' +} + +#flowvisor_topic.on_message proc { |m| m.operation != :inform } do |message| +# logger.warn message +#end + +# messages { key: Topic } +msgs = { + create_1: comm.create_message([type: 'openflow_slice', name: 'test1']), + create_2: comm.create_message([type: 'openflow_slice', name: 'test2', controller_port: '9934']), + + config_1a: comm.configure_message([{flows: {operation: 'add', device: 'all', eth_src: port2eth[16]}}]), + config_1b: comm.configure_message([{flows: {operation: 'add', device: 'all', eth_src: port2eth[21]}}]), + config_1c: comm.configure_message([{flows: {operation: 'add', device: '00:00:00:00:00:00:00:02', in_port: '1', eth_src: port2eth[16]}}]), + config_1d: comm.configure_message([{flows: {operation: 'add', device: '00:00:00:00:00:00:00:01', in_port: '1', eth_src: port2eth[21]}}]), + + config_2a: comm.configure_message([{flows: {operation: 'add', device: '00:00:00:00:00:00:00:01', in_port: '15'}}]), + config_2b: comm.configure_message([{flows: {operation: 'add', device: '00:00:00:00:00:00:00:02', in_port: '23'}}]), + config_2c: comm.configure_message([{flows: {operation: 'add', device: '00:00:00:00:00:00:00:02', in_port: '1', eth_src: port2eth[15]}}]), + config_2d: comm.configure_message([{flows: {operation: 'add', device: '00:00:00:00:00:00:00:01', in_port: '1', eth_src: port2eth[23]}}]), +} + +%w{create_1 create_2}.each do |s| + msgs[s.to_sym].on_inform_failed do |message| + logger.error "Resource creation failed ---" + logger.error message.read_content("reason") + end +end + +%w{config_1a config_1b config_1c config_1d config_2a config_2b config_2c config_2d}.each do |s| + msgs[s.to_sym].on_inform_status do |message| + message.each_property do |p| + logger.info "#{p.attr('key')} => #{p.content.strip}" + end + end +end + +msgs[:create_1].on_inform_created do |message| + slice_topic = comm.get_topic(message.resource_id) + slice_id = slice_topic.id + + msgs[:release_1] ||= comm.release_message { |m| m.element('resource_id', slice_id) } + + msgs[:release_1].on_inform_released do |message| + logger.info "Slice (#{message.resource_id}) deleted (resource released)" + end + + logger.info "Slice #{slice_id} ready for testing" + + slice_topic.subscribe do + msgs[:config_1a].publish slice_id +# msgs[:config_1b].publish slice_id +# msgs[:config_1c].publish slice_id +# msgs[:config_1d].publish slice_id + end +end + +msgs[:create_2].on_inform_created do |message| + slice_topic = comm.get_topic(message.resource_id) + slice_id = slice_topic.id + + msgs[:release_2] ||= comm.release_message { |m| m.element('resource_id', slice_id) } + + msgs[:release_2].on_inform_released do |message| + logger.info "Slice (#{message.resource_id}) deleted (resource released)" + end + + logger.info "Slice #{slice_id} ready for testing" + + slice_topic.subscribe do + msgs[:config_2a].publish slice_id +# msgs[:config_2b].publish slice_id +# msgs[:config_2c].publish slice_id +# msgs[:config_2d].publish slice_id + end +end + +# Then we can register event handlers to the communicator +# +# Event triggered when connection is ready +comm.when_ready do + logger.info "CONNECTED: #{comm.jid.inspect}" + + # We assume that a openflow_slice_factory (flowvisor) resource proxy instance is up already, so we subscribe to its pubsub topic + flowvisor_topic.subscribe do + # If subscribed, we publish a 'create' message, 'create' a new openflow slice for testing + msgs[:create_1].publish flowvisor_id + msgs[:create_2].publish flowvisor_id + end +end + +EM.run do + comm.connect(options[:user], options[:password], options[:server]) + + trap(:INT) do + msgs[:release_1].publish flowvisor_id + msgs[:release_2].publish flowvisor_id + comm.disconnect + end + trap(:TERM) do + msgs[:release_1].publish flowvisor_id + msgs[:release_2].publish flowvisor_id + comm.disconnect + end +end diff --git a/bin/openflow_slice_factory_controller b/bin/openflow_slice_factory_controller new file mode 100755 index 0000000..d02c67b --- /dev/null +++ b/bin/openflow_slice_factory_controller @@ -0,0 +1,22 @@ +#!/usr/bin/env ruby + +require 'omf_rc' +require 'omf_rc/resource_factory' +require 'omf_rc/resource_proxy/openflow_slice_factory' +require 'omf_rc/resource_proxy/openflow_slice' +$stdout.sync = true + +options = { + user: 'testbed', + password: 'pw', + server: 'srv.mytestbed.net', # XMPP pubsub server domain + uid: 'flowvisor', +} + +EM.run do + openflowslicefactory = OmfRc::ResourceFactory.new(:openflow_slice_factory, options) + openflowslicefactory.connect + + trap(:INT) { openflowslicefactory.disconnect } + trap(:TERM) { openflowslicefactory.disconnect } +end diff --git a/bin/openflow_test b/bin/openflow_test new file mode 100755 index 0000000..b8a1683 --- /dev/null +++ b/bin/openflow_test @@ -0,0 +1,101 @@ +#!/usr/bin/env ruby + +require 'omf_common' +$stdout.sync = true + +include OmfCommon + +options = { + user: 'user', + password: 'pw', + server: 'srv.mytestbed.net', # XMPP pubsub server domain + uid: 'flowvisor', + debug: false +} + +flowvisor_id = options[:uid] + +Logging.logger.root.level = options[:debug] ? :debug : :info +Blather.logger = logger + +# We will use Comm directly, with default DSL implementaion :xmpp +comm = Comm.new(:xmpp) + +flowvisor_topic = comm.get_topic(flowvisor_id) + +#flowvisor_topic.on_message proc { |m| m.operation != :inform } do |message| +# logger.warn message +#end + +# messages { key: Topic } +msgs = { + create: comm.create_message([type: 'openflow_slice', name: 'test1']), + config_a: comm.configure_message([flows: {operation: 'add', device: '00:00:00:00:00:00:00:01', in_port: '16'}]), +# config_b: comm.configure_message([flows: {operation: 'add', device: '00:00:00:00:00:00:00:02', in_port: '21'}]), +# config_c: comm.configure_message([flows: {operation: 'add', device: '00:00:00:00:00:00:00:02', in_port: '23'}]), +# config_d: comm.configure_message([flows: {operation: 'add', device: '00:00:00:00:00:00:00:01', in_port: '15'}]), +# config_e: comm.configure_message([flows: {operation: 'add', device: '00:00:00:00:00:00:00:01', in_port: '1', eth_src: '00:03:2d:0d:30:d4'}]), +# config_f: comm.configure_message([flows: {operation: 'add', device: '00:00:00:00:00:00:00:02', in_port: '1', eth_src: '00:03:2d:0d:30:c0'}]), +} + +msgs[:create].on_inform_failed do |message| + logger.error "Resource creation failed ---" + logger.error message.read_content("reason") +end + +%w{config_a}.each do |s| +#config_b config_c config_d config_e config_f}.each do |s| + msgs[s.to_sym].on_inform_status do |message| + message.each_property do |p| + logger.info "#{p.attr('key')} => #{p.content.strip}" + end + end +end + +msgs[:create].on_inform_created do |message| + slice_topic = comm.get_topic(message.resource_id) + slice_id = slice_topic.id + + msgs[:release] ||= comm.release_message { |m| m.element('resource_id', slice_id) } + + msgs[:release].on_inform_released do |message| + logger.info "Slice (#{message.resource_id}) deleted (resource released)" + end + + logger.info "Slice #{slice_id} ready for testing" + + slice_topic.subscribe do + msgs[:config_a].publish slice_id +# msgs[:config_b].publish slice_id +# msgs[:config_c].publish slice_id +# msgs[:config_d].publish slice_id +# msgs[:config_e].publish slice_id +# msgs[:config_f].publish slice_id + end +end + +# Then we can register event handlers to the communicator +# +# Event triggered when connection is ready +comm.when_ready do + logger.info "CONNECTED: #{comm.jid.inspect}" + + # We assume that a openflow_slice_factory (flowvisor) resource proxy instance is up already, so we subscribe to its pubsub topic + flowvisor_topic.subscribe do + # If subscribed, we publish a 'create' message, 'create' a new openflow slice for testing + msgs[:create].publish flowvisor_id + end +end + +EM.run do + comm.connect(options[:user], options[:password], options[:server]) + + trap(:INT) do + msgs[:release].publish flowvisor_id + comm.disconnect + end + trap(:TERM) do + msgs[:release].publish flowvisor_id + comm.disconnect + end +end diff --git a/lib/omf_rc/resource_proxy/openflow_slice.rb b/lib/omf_rc/resource_proxy/openflow_slice.rb new file mode 100644 index 0000000..eb91a9e --- /dev/null +++ b/lib/omf_rc/resource_proxy/openflow_slice.rb @@ -0,0 +1,79 @@ +# This resource is created from the parent :openflow_slice_factory resource. +# It is related with a slice of a flowvisor instance, and behaves as a proxy between experimenter and the actual flowvisor slice. +# +module OmfRc::ResourceProxy::OpenflowSlice + include OmfRc::ResourceProxyDSL + + # The default parameters of a new slice. The openflow controller is assumed to be in the same working station with flowvisor instance + SLICE_DEFAULTS = { + passwd: "1234", + url: "tcp:127.0.0.1:9933", + email: "nothing@nowhere" + } + + + register_proxy :openflow_slice + + utility :openflow_tools + + + # Slice's name is initiated with value "nil" + hook :before_ready do |resource| + resource.property.name = nil + end + + # Before release, the related flowvisor instance should also remove the corresponding slice + hook :before_release do |resource| + resource.flowvisor_connection.call("api.deleteSlice", resource.property.name) + end + + + # The name is one-time configured + configure :name do |resource, name| + raise "The name cannot be changed" if resource.property.name + resource.property.name = name.to_s + begin + resource.flowvisor_connection.call("api.createSlice", name.to_s, *SLICE_DEFAULTS.values) + rescue Exception => e + if e.message["Cannot create slice with existing name"] + logger.warn message = "The requested slice already existed in Flowvisor" + else + raise e + end + end + resource.property.name + end + + # Configures the slice password + configure :passwd do |resource, passwd| + resource.flowvisor_connection.call("api.changePasswd", resource.property.name, passwd.to_s) + passwd.to_s + end + + # Configures the slice parameters + [:contact_email, :drop_policy, :controller_hostname, :controller_port].each do |configure_sym| + configure configure_sym do |resource, value| + resource.flowvisor_connection.call("api.changeSlice", resource.property.name, configure_sym.to_s, value.to_s) + value.to_s + end + end + + # Adds/removes a flow to this slice, specified by device, port, etc. + configure :flows do |resource, parameters| + resource.flowvisor_connection.call("api.changeFlowSpace", resource.transformed_parameters(parameters)) + resource.flows + end + + + # Returns a hash table with the name of this slice, its controller (ip and port) and other related information + request :info do |resource| + result = resource.flowvisor_connection.call("api.getSliceInfo", resource.property.name) + result[:name] = resource.property.name + result + end + + # Returns a string with statistics about the use of this slice + request :stats do |resource| + resource.flowvisor_connection.call("api.getSliceStats", resource.property.name) + end +end diff --git a/lib/omf_rc/resource_proxy/openflow_slice_factory.rb b/lib/omf_rc/resource_proxy/openflow_slice_factory.rb new file mode 100644 index 0000000..96e93b7 --- /dev/null +++ b/lib/omf_rc/resource_proxy/openflow_slice_factory.rb @@ -0,0 +1,71 @@ +# This resourse is related with a flowvisor instance and behaves as a proxy between experimenter and flowvisor. +# +module OmfRc::ResourceProxy::OpenflowSliceFactory + include OmfRc::ResourceProxyDSL + + # The default arguments of the communication between this resource and the flowvisor instance + FLOWVISOR_CONNECTION_DEFAULTS = { + host: "localhost", + path: "/xmlrc", + port: "8080", + proxy_host: nil, + proxy_port: nil, + user: "fvadmin", + password: "openflow", + use_ssl: "true", + timeout: nil + } + + + register_proxy :openflow_slice_factory + + utility :openflow_tools + + + # Checks if the created child is an :openflow_slice resource and passes the connection arguments that are essential for the connection with flowvisor instance + hook :before_create do |resource, type, opts = nil| + if type.to_sym != :openflow_slice + raise "This resource doesn't create resources of type "+type + end + begin + resource.flowvisor_connection + rescue + raise "This resource is not connected with a flowvisor instance, so it cannot create openflow slices" + end + opts.property ||= Hashie::Mash.new + opts.property.flowvisor_connection_args = resource.property.flowvisor_connection_args + end + + + # A new resource uses the default connection arguments (ip adress, port, etc) to connect with a flowvisor instance + hook :before_ready do |resource| + resource.property.flowvisor_connection_args = FLOWVISOR_CONNECTION_DEFAULTS + end + + + # Configures the flowvisor connection arguments (ip adress, port, etc) + configure :flowvisor_connection do |resource, flowvisor_connection_args| + raise "Connection with a new flowvisor instance is not allowed if there exist created slices" if !resource.children.empty? + resource.property.flowvisor_connection_args.update(flowvisor_connection_args) + end + + + # Returns the flowvisor connection arguments (ip adress, port, etc) + request :flowvisor_connection do |resource| + resource.property.flowvisor_connection_args + end + + # Returns a list of the existed slices or the connected devices + {:slices => "listSlices", :devices => "listDevices"}.each do |request_sym, handler_name| + request request_sym do |resource| + resource.flowvisor_connection.call("api.#{handler_name}") + end + end + + # Returns information or statistics for a device specified by the given id + {:device_info => "getDeviceInfo", :device_stats => "getSwitchStats"}.each do |request_sym, handler_name| + request request_sym do |resource, device| + resource.flowvisor_connection.call("api.#{handler_name}", device.to_s) + end + end +end diff --git a/lib/omf_rc/util/openflow_tools.rb b/lib/omf_rc/util/openflow_tools.rb new file mode 100644 index 0000000..76c46ec --- /dev/null +++ b/lib/omf_rc/util/openflow_tools.rb @@ -0,0 +1,103 @@ +require 'xmlrpc/client' + +module OmfRc::Util::OpenflowTools + include OmfRc::ResourceProxyDSL + + # The version of the flowvisor that this resource is able to control + FLOWVISOR_VERSION = "FV version=flowvisor-0.8.4" + + # Parts of the regular expression that describes a flow entry for flowvisor + FLOWVISOR_FLOWENTRY_REGEXP_DEVIDED = [ + /dpid=\[(?.+)\]/, + /ruleMatch=\[OFMatch\[(?.+)\]\]/, + /actionsList=\[Slice:(?.+)=(?.+)\]/, + /id=\[(?.+)\]/, + /priority=\[(?.+)\]/ + ] + + # The regular expression that describes a flow entry for flowvisor + FLOWVISOR_FLOWENTRY_REGEXP = /FlowEntry\[#{FLOWVISOR_FLOWENTRY_REGEXP_DEVIDED.join(',')},\]/ + + # The names of the flow (or flow entry) features + FLOW_FEATURES = %w{device match slice actions id priority} + + # The names of the flow (or flow entry) features that are specified by the "match" feature + FLOW_MATCH_FEATURES = %w{in_port eth_src eth_dst ip_src ip_dst} + + # The default features of a new flow (or flow entry) + FLOW_DEFAULTS = { + priority: "10", + actions: "4" + } + + + # Returns the flows (flow entries) that exist for this flowvisor + request :flows do |resource, filter = nil| + resource.flows(filter) + end + + + # Internal function that creates a connection with a flowvisor instance and checks it + work :flowvisor_connection do |resource| + xmlrpc_client = XMLRPC::Client.new_from_hash(resource.property.flowvisor_connection_args) + xmlrpc_client.instance_variable_get("@http").verify_mode = OpenSSL::SSL::VERIFY_NONE + ping_msg = "test" + pong_msg = "PONG(#{resource.property.flowvisor_connection_args[:user]}): #{FLOWVISOR_VERSION}::#{ping_msg}" + raise "Connection with #{FLOWVISOR_VERSION} was not successful" if xmlrpc_client.call("api.ping", ping_msg) != pong_msg + xmlrpc_client + end + + # Internal function that returns the flows (flow entries) that exist in the connected flowvisor instance + work :flows do |resource, filter = nil| + result = resource.flowvisor_connection.call("api.listFlowSpace") + result.map! do |line| + array_values = line.match(FLOWVISOR_FLOWENTRY_REGEXP)[1..-1] + # Example of above array's content: %w{00:00:...:01 in_port=1 test 4 30 10} + array_features_values_zipped = FLOW_FEATURES.zip(array_values) + # Example of above array's content: %w{device 00:00:...:01 match in_port=1 slice test actions 4 id 30 priority 10} + hash = Hashie::Mash.new(Hash[array_features_values_zipped]) + # The following code adds extra features that are specified by the "match" feature + hash["match"].split(",").each do |couple| + array = couple.split("=") + hash[array[0]] = array[1] + end + hash + end + result.delete_if {|hash| hash["slice"] != resource.property.name} if resource.type.to_sym == :openflow_slice + FLOW_FEATURES.each do |feature| + result.delete_if {|hash| hash[feature] != filter[feature].to_s} if filter[feature] + end if filter + result + end + + work :transformed_parameters do |resource, parameters| + + match = [] + FLOW_MATCH_FEATURES.each do |feature| + match << "#{feature}=#{parameters[feature]}" if parameters[feature] + end + match = match.join(",") + + result = [] + case parameters.operation + when "add" + h = Hashie::Mash.new + h.operation = parameters.operation.upcase + h.priority = parameters.priority ? parameters.priority.to_s : FLOW_DEFAULTS[:priority] + h.dpid = parameters.device.to_s + h.actions = "Slice:#{resource.property.name}=#{(parameters.actions ? parameters.actions : FLOW_DEFAULTS[:actions])}" + h.match = "OFMatch[#{match}]" + result << h + when "remove" + resource.flows(parameters).each do |f| + if f.match == match + h = Hashie::Mash.new + h.operation = parameters.operation.upcase + h.id = f.id + result << h + end + end + end + result + end +end diff --git a/lib/omf_rc_openflow.rb b/lib/omf_rc_openflow.rb new file mode 100644 index 0000000..14d015d --- /dev/null +++ b/lib/omf_rc_openflow.rb @@ -0,0 +1,5 @@ +require "omf_rc_openflow/version" + +require 'omf_rc/resource_proxy/openflow_slice_factory.rb' +require 'omf_rc/resource_proxy/openflow_slice.rb' +require 'omf_rc/util/openflow_tools.rb' diff --git a/lib/omf_rc_openflow/version.rb b/lib/omf_rc_openflow/version.rb new file mode 100644 index 0000000..0856cd2 --- /dev/null +++ b/lib/omf_rc_openflow/version.rb @@ -0,0 +1,3 @@ +module OmfRcOpenflow + VERSION = "6.0.0" +end diff --git a/omf_rc_openflow.gemspec b/omf_rc_openflow.gemspec new file mode 100644 index 0000000..4ba7eaf --- /dev/null +++ b/omf_rc_openflow.gemspec @@ -0,0 +1,18 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/omf_rc_openflow/version', __FILE__) + +Gem::Specification.new do |gem| + gem.authors = ["Kostas Choumas"] + gem.email = ["kohoumas@gmail.com"] + gem.description = %q{OMF Resource Controllers related to Openflow} + gem.summary = %q{OMF Resource Controllers related to Openflow} + gem.homepage = "" + + gem.files = `git ls-files`.split($\) + gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } + gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) + gem.name = "omf_rc_openflow" + gem.require_paths = ["lib"] + gem.version = OmfRcOpenflow::VERSION + gem.add_runtime_dependency "omf_rc", "~> 6.0.0.pre" +end