diff --git a/Gemfile.lock b/Gemfile.lock index bbbe33b..5be8db2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,9 +3,11 @@ PATH specs: phut (0.7.7) activesupport (~> 4.2.6) + ffi-yajl (~> 2.2.3) gli (~> 2.13.4) pio (~> 0.30.0) pry (~> 0.10.3) + yajl-ruby (~> 1.2.1) GEM remote: https://rubygems.org/ @@ -69,6 +71,8 @@ GEM faker (1.6.3) i18n (~> 0.5) ffi (1.9.10) + ffi-yajl (2.2.3) + libyajl2 (~> 1.2) flay (2.8.0) erubis (~> 2.7.0) path_expander (~> 1.0) @@ -113,6 +117,7 @@ GEM i18n (0.7.0) ice_nine (0.11.2) json (1.8.3) + libyajl2 (1.2.0) listen (3.0.8) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -133,6 +138,7 @@ GEM pio (0.30.0) activesupport (~> 4.2, >= 4.2.4) bindata (~> 2.1.0) + bundler (~> 1.11.2) powerpack (0.1.1) pry (0.10.3) coderay (~> 1.1.0) @@ -205,6 +211,7 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) + yajl-ruby (1.2.1) yard (0.8.7.6) PLATFORMS diff --git a/features/shell/vswitch#ports.feature b/features/shell/vswitch#ports.feature index 128bf0c..bccdb87 100644 --- a/features/shell/vswitch#ports.feature +++ b/features/shell/vswitch#ports.feature @@ -11,7 +11,10 @@ Feature: Vswitch#ports Scenario: Vswitch#ports #=> [] When I type "vswitch.ports" And sleep 5 - Then the output should contain "[]" + Then the output should contain: + """ + [] + """ @sudo Scenario: Vswitch#ports @@ -20,9 +23,9 @@ Feature: Vswitch#ports When I type "vswitch.ports" And sleep 5 Then the output should contain: - """ - ["L0_a"] - """ + """ + [#] + """ @sudo Scenario: Vswitch#ports @@ -31,6 +34,6 @@ Feature: Vswitch#ports When I type "vswitch.ports" And sleep 5 Then the output should contain: - """ - ["L0_a"] - """ + """ + [#] + """ diff --git a/lib/phut.rb b/lib/phut.rb index 192c960..1bf5f35 100644 --- a/lib/phut.rb +++ b/lib/phut.rb @@ -5,3 +5,4 @@ require 'phut/version' require 'phut/vhost_daemon' require 'phut/vswitch' +require 'phut/ovsdb' diff --git a/lib/phut/open_vswitch.rb b/lib/phut/open_vswitch.rb index a3f8add..5b0ebeb 100644 --- a/lib/phut/open_vswitch.rb +++ b/lib/phut/open_vswitch.rb @@ -123,6 +123,7 @@ def <=>(other) private def start + @vsctl.set_manager @vsctl.add_bridge @vsctl.set_openflow_version_and_dpid @vsctl.controller_tcp_port = @tcp_port diff --git a/lib/phut/ovsdb.rb b/lib/phut/ovsdb.rb new file mode 100644 index 0000000..7130591 --- /dev/null +++ b/lib/phut/ovsdb.rb @@ -0,0 +1,7 @@ +require 'phut/ovsdb/client' +require 'phut/ovsdb/transaction' + +module Phut + # OVSDB client core + module OVSDB; end +end diff --git a/lib/phut/ovsdb/README.md b/lib/phut/ovsdb/README.md new file mode 100644 index 0000000..929d58a --- /dev/null +++ b/lib/phut/ovsdb/README.md @@ -0,0 +1,100 @@ + +Primitive OVSDB client implementation +=== + +Supports [RFC7047](https://tools.ietf.org/html/rfc7047). + +## Examples + +### Transaction + +* Create Bridge + +```ruby +require 'active_flow' + +class OVSDBTest + extend ActiveFlow::OVSDB::Transact + + def self.create_bridge(name, ofc_target, bridge_options = {}) + client = ActiveFlow::OVSDB::Client.new('localhost', 6632) + ovs_rows_query = select('Open_vSwitch', [], [:_uuid, :bridges]) + ovs_row = client.transact(1, 'Open_vSwitch', [ovs_rows_query]).first[:rows].first + ovs_bridges = ovs_row[:bridges] + new_ovs_bridges = case ovs_bridges.include?('set') + when true + ovs_bridges_content = ovs_bridges[1] + case ovs_bridges_content.empty? + when true + ['named-uuid', "bridge_br_#{name}"] + else + ['set', ovs_bridges_content << ['named-uuid', "bridge_br_#{name}"]] + end + else + ovs_bridges_content = ovs_bridges[1] + ['set', [ovs_bridges_content] << ['named-uuid', "bridge_br_#{name}"]] + end + ovs_uuid = ovs_row[:_uuid] + interface = { name: "br-#{name}", type: "internal" } + port = { name: "br-#{name}", interfaces: ['named-uuid', "interface_br_#{name}"] } + controller = { target: ofc_target } + bridge = { name: "br-#{name}", ports: ['named-uuid', "port_br_#{name}"], controller: ['named-uuid', "ofc_br_#{name}"], protocols: 'OpenFlow10' } + transactions = [ + insert('Interface', interface, "interface_br_#{name}"), + insert('Port', port, "port_br_#{name}"), + insert('Controller', controller, "ofc_br_#{name}"), + insert('Bridge', bridge, "bridge_br_#{name}"), + update('Open_vSwitch', [[:_uuid, :==, ovs_uuid]], { bridges: new_ovs_bridges }), + mutate('Open_vSwitch', [[:_uuid, :==, ovs_uuid]], [[:next_cfg, '+=', 1]]) + ] + client.transact(1, 'Open_vSwitch', transactions) + transactions = [ + update('Bridge', [[:name, :==, "br-#{name}"]], { other_config: [:map, bridge_options.to_a] }), + mutate('Open_vSwitch', [[:_uuid, :==, ovs_uuid]], [[:next_cfg, '+=', 1]]) + ] + client.transact(1, 'Open_vSwitch', transactions) + end + + def self.connect_with_patch(br1, br2) + patch_br1 = "patch-#{br1}" + patch_br2 = "patch-#{br2}" + client = ActiveFlow::OVSDB::Client.new('localhost', 6632) + ovs_rows_query = select('Open_vSwitch', [], [:_uuid]) + ovs_row = client.transact(1, 'Open_vSwitch', [ovs_rows_query]).first[:rows].first + ovs_uuid = ovs_row[:_uuid] + selects = [ + select('Bridge', [[:name, :==, br1]], [:ports]), + select('Bridge', [[:name, :==, br2]], [:ports]) + ] + br1_ports, br2_ports = client.transact(1, 'Open_vSwitch', selects) + new_br1_ports = br1_ports.map do |_, item| + ports = item[0][:ports].include?('set') ? item[0][:ports][1] : [item[0][:ports]] + [:set, ports << ['named-uuid', :patch_br1]] + end.first + new_br2_ports = br2_ports.map do |_, item| + ports = item[0][:ports].include?('set') ? item[0][:ports][1] : [item[0][:ports]] + [:set, ports << ['named-uuid', :patch_br2]] + end.first + + patch_br1_port = {name: patch_br1, interfaces: ['named-uuid', :patch_br1_iface]} + patch_br2_port = {name: patch_br2, interfaces: ['named-uuid', :patch_br2_iface]} + + patch_br1_iface = {name: patch_br1, type: :patch, options: [:map, {peer: patch_br2}.to_a]} + patch_br2_iface = {name: patch_br2, type: :patch, options: [:map, {peer: patch_br1}.to_a]} + + transactions = [ + insert('Interface', patch_br1_iface, :patch_br1_iface), + insert('Interface', patch_br2_iface, :patch_br2_iface), + insert('Port', patch_br1_port, :patch_br1), + insert('Port', patch_br2_port, :patch_br2), + update('Bridge', [[:name, :==, br1]], { ports: new_br1_ports }), + update('Bridge', [[:name, :==, br2]], { ports: new_br2_ports }), + mutate('Open_vSwitch', [[:_uuid, :==, ovs_uuid]], [[:next_cfg, '+=', 1]]) + ] + client.transact(1, 'Open_vSwitch', transactions) + end +end + +# OVSDBTest.create_bridge('def', 'tcp:127.0.0.1:6653', 'datapath-id' => '0000000000000def') +# OVSDBTest.connect_with_patch('nts0xabc', 'br-def') +``` diff --git a/lib/phut/ovsdb/client.rb b/lib/phut/ovsdb/client.rb new file mode 100644 index 0000000..c9510b2 --- /dev/null +++ b/lib/phut/ovsdb/client.rb @@ -0,0 +1,74 @@ +require 'phut/ovsdb/method' +require 'phut/ovsdb/transport' +require 'yajl' + +module Phut + module OVSDB + # OVSDB Client core + class Client + include Phut::OVSDB::Method + + attr_reader :transport + attr_reader :database + + def initialize(host, port, options = {}) + @mut = Mutex.new + @queue = Queue.new + @transport = Transport.new(host, port, self, options) + @database = options.fetch(:database, nil) + initialize_codec + end + + def handle_tcp(data) + @parser << data + end + + def handle_message(data) + case data[:method] + when 'echo' + echo_reply + else + maybe_handle_reply(data) + end + end + + private + + def maybe_handle_reply(data) + id = data[:id] + case id + when 'echo' + :noop + else + @queue.enq(data) + end + end + + def initialize_codec + @parser = Yajl::Parser.new(symbolize_keys: true) + @parser.on_parse_complete = method(:handle_message) + @encoder = Yajl::Encoder.new + end + + def json_async_send(jsonable) + json_data = @encoder.encode(jsonable) + transport.send(json_data) + end + + def json_send(jsonable) + json_async_send(jsonable) + th = Thread.new do + result = nil + continue = true + while continue + next if @queue.empty? + @mut.synchronize { result = @queue.deq } + continue = false + end + result + end + th.join.value + end + end + end +end diff --git a/lib/phut/ovsdb/method.rb b/lib/phut/ovsdb/method.rb new file mode 100644 index 0000000..b9acb40 --- /dev/null +++ b/lib/phut/ovsdb/method.rb @@ -0,0 +1,26 @@ +require 'e2mmap' + +module Phut + module OVSDB + # OVSDB methods + module Method + extend Exception2MessageMapper + + def_exception :GetSchemaError, '%s' + def_exception :TransactionError, '%s' + + def echo_reply + json_async_send(id: 'echo', result: [], error: nil) + end + + def transact(id, db_name, operations) + data = json_send( + id: id, + method: 'transact', + params: [db_name, *operations] + ) + data[:result] + end + end + end +end diff --git a/lib/phut/ovsdb/transaction.rb b/lib/phut/ovsdb/transaction.rb new file mode 100644 index 0000000..1502aab --- /dev/null +++ b/lib/phut/ovsdb/transaction.rb @@ -0,0 +1,92 @@ +module Phut + module OVSDB + # Transaction + module Transaction + def select(table, conds, cols = nil) + select = { + op: :select, + table: table, + where: Array(conds) + } + select = { columns: Array(cols) }.merge(select) if cols + select + end + + def insert(table, row, uuid_name = nil) + insert = { + op: :insert, + table: table, + row: row + } + insert = { 'uuid-name' => uuid_name }.merge(insert) if uuid_name + insert + end + + def update(table, conds, row) + { + op: :update, + table: table, + where: Array(conds), + row: row + } + end + + def delete(table, conds) + { + op: :delete, + table: table, + where: Array(conds) + } + end + + def mutate(table, conds, mutes) + { + op: :mutate, + table: table, + where: Array(conds), + mutations: Array(mutes) + } + end + + def commit(mode = true) + { + op: :commit, + durable: mode + } + end + + def abort + { + op: :abort + } + end + + def wait(table, cond, cols, until_cond, rows, timeout = nil) + wait = { + op: :wait, + table: table, + where: Array(cond), + columns: Array(cols), + until: until_cond, + rows: Array(rows) + } + wait = { timeout: timeout }.merge(wait) if timeout + wait + end + + def comment(string) + { + op: :comment, + comment: string + } + end + + def assert(lock_id) + { + op: :assert, + lock: lock_id + } + end + end + end +end diff --git a/lib/phut/ovsdb/transport.rb b/lib/phut/ovsdb/transport.rb new file mode 100644 index 0000000..0444f6c --- /dev/null +++ b/lib/phut/ovsdb/transport.rb @@ -0,0 +1,77 @@ +require 'socket' + +module Phut + module OVSDB + # OVSDB Transport Class + class Transport + attr_reader :host + attr_reader :port + attr_reader :options + attr_reader :callback + + def initialize(host, port, klass, options = {}) + @options = options + @host = host + @port = port + @callback = klass.method(:handle_tcp) + signal_connect + enter_loop + end + + def send(data) + @socket.write(data) + rescue Errno::EPIPE + signal_connect + end + + def read + @socket.readpartial(1000) + end + + private + + def enter_loop + Thread.abort_on_exception = true + Thread.new do + loop do + begin + handle_connection + rescue => error + raise error + end + end + end + end + + def signal_connect + count = count.to_i + 1 + @socket = TCPSocket.open(host, port) + rescue => error + sleep reconnect_interval + should_reconnect?(count) ? retry : raise(error) + end + + def should_reconnect?(count) + (reconnect && (reconnect_retry_limit > count)) + end + + def handle_connection + callback.call(read) + rescue EOFError + signal_connect + end + + def reconnect + options[:reconnect] ||= false + end + + def reconnect_interval + options[:reconnect_interval] ||= 5 + end + + def reconnect_retry_limit + options[:reconnect_retry_limit] ||= 10 + end + end + end +end diff --git a/lib/phut/port.rb b/lib/phut/port.rb new file mode 100644 index 0000000..388198e --- /dev/null +++ b/lib/phut/port.rb @@ -0,0 +1,21 @@ +module Phut + # OpenvSwitch Port class + class Port + + attr_reader :device + attr_reader :number + + def initialize(device:, number:) + @device = device + @number = number + end + + def inspect + "#" + end + + def to_s + "port (number = #{number}, device = #{device})" + end + end +end diff --git a/lib/phut/vsctl.rb b/lib/phut/vsctl.rb index 9b7e829..de41c81 100644 --- a/lib/phut/vsctl.rb +++ b/lib/phut/vsctl.rb @@ -1,11 +1,15 @@ # frozen_string_literal: true require 'phut/shell_runner' +require 'phut/ovsdb' +require 'phut/port' require 'pio' module Phut # ovs-vsctl wrapper class Vsctl extend ShellRunner + include Phut::OVSDB::Transaction + extend Phut::OVSDB::Transaction def self.list_br(prefix) sudo('ovs-vsctl list-br').split.each_with_object([]) do |each, list| @@ -18,6 +22,7 @@ def self.list_br(prefix) include ShellRunner def initialize(name:, name_prefix:, dpid:, bridge:) + @client = Phut::OVSDB::Client.new('localhost', 6632) @name = name @prefix = name_prefix @dpid = dpid @@ -30,12 +35,16 @@ def tcp_port end def add_bridge - sudo "ovs-vsctl add-br #{@bridge}" + sudo "ovs-vsctl --may-exist add-br #{@bridge}" sudo "/sbin/sysctl -w net.ipv6.conf.#{@bridge}.disable_ipv6=1 -q" end def del_bridge - sudo "ovs-vsctl del-br #{@bridge}" + sudo "ovs-vsctl --if-exists del-br #{@bridge}" + end + + def set_manager + sudo 'ovs-vsctl set-manager ptcp:6632' end def set_openflow_version_and_dpid @@ -55,14 +64,14 @@ def set_fail_mode_secure end def add_port(device) - sudo "ovs-vsctl add-port #{@bridge} #{device}" + sudo "ovs-vsctl --may-exist add-port #{@bridge} #{device}" nil end def add_numbered_port(port_number, device) add_port device - sudo "ovs-vsctl set Port #{device} "\ - "other_config:rstp-port-num=#{port_number}" + sudo "ovs-vsctl set Interface #{device} "\ + "ofport_request=#{port_number}" nil end @@ -75,7 +84,31 @@ def bring_port_down(port_number) end def ports - sudo("ovs-vsctl list-ports #{@bridge}").split + br_query = [select('Bridge', [[:name, :==, @bridge]], [:ports])] + br_ports = @client.transact(1, 'Open_vSwitch', br_query) + if br_ports.first[:rows].first + br_ports = br_ports.first[:rows].first[:ports] + ports = if br_ports.include? "set" + br_ports[1] + else + [br_ports] + end + port_query = ports.map do |port| + select('Port', [[:_uuid, :==, port]], [:name]) + end + iface_query = @client.transact(1, 'Open_vSwitch', port_query).map do |iface| + select('Interface', [[:name, :==, iface[:rows].first[:name]], + [:name, :!=, @bridge]], [:ofport, :name]) + end + @client.transact(1, 'Open_vSwitch', iface_query).map do |iface| + next unless iface[:rows].first.respond_to?(:[]) + device = iface[:rows].first[:name] + number = iface[:rows].first[:ofport] + Phut::Port.new(device: device, number: number) + end.compact + else + [] + end end private diff --git a/phut.gemspec b/phut.gemspec index cb1c959..c45cd32 100644 --- a/phut.gemspec +++ b/phut.gemspec @@ -28,4 +28,6 @@ Gem::Specification.new do |gem| gem.add_dependency 'gli', '~> 2.13.4' gem.add_dependency 'pio', '~> 0.30.0' gem.add_dependency 'pry', '~> 0.10.3' + gem.add_dependency 'ffi-yajl', '~> 2.2.3' + gem.add_dependency 'yajl-ruby', '~> 1.2.1' end