--- p2v/client/ext/rblibssh2/rblibssh2_channel.c | 131 +++++++++++++++++++++++++++ p2v/client/lib/virt-p2v/blockdevice.rb | 13 ++- p2v/client/lib/virt-p2v/connection.rb | 16 ++++ p2v/client/lib/virt-p2v/converter.rb | 32 ++++++- p2v/client/lib/virt-p2v/ui/convert.rb | 44 ++++++--- p2v/client/lib/virt-p2v/ui/p2v.ui | 22 ++++- p2v/server/virt-p2v-server.pl | 31 +++++++ 7 files changed, 268 insertions(+), 21 deletions(-) diff --git a/p2v/client/ext/rblibssh2/rblibssh2_channel.c b/p2v/client/ext/rblibssh2/rblibssh2_channel.c index 285da17..f648d20 100644 --- a/p2v/client/ext/rblibssh2/rblibssh2_channel.c +++ b/p2v/client/ext/rblibssh2/rblibssh2_channel.c @@ -39,20 +39,24 @@ static VALUE eApplicationError; static VALUE eShortReadError; static VALUE session_exec(VALUE self_rv, VALUE cmd_rv); +static VALUE session_forward(VALUE self_rv, VALUE port_rv); static VALUE channel_read(VALUE self_rv, VALUE bytes_rv); static VALUE channel_write(VALUE self_rv, VALUE data_rv); static VALUE channel_send_data(VALUE self_rv, VALUE io_rv); +static VALUE channel_forward(VALUE self_rv, VALUE io_rv); static VALUE channel_close(VALUE self_rv); void rblibssh2_channel_init() { rb_define_method(cSession, "exec", session_exec, 1); + rb_define_method(cSession, "forward_port", session_forward, 1); cChannel = rb_define_class_under(cLibssh2, "Channel", rb_cObject); rb_define_method(cChannel, "read", channel_read, 1); rb_define_method(cChannel, "write", channel_write, 1); rb_define_method(cChannel, "send_data", channel_send_data, 1); + rb_define_method(cChannel, "forward", channel_forward, 1); rb_define_method(cChannel, "close", channel_close, 0); eApplicationError = rb_define_class_under(cChannel, "ApplicationError", @@ -138,6 +142,60 @@ static VALUE session_exec(VALUE self_rv, VALUE cmd_rv) return channel_rv; } +struct session_forward { + struct channel *c; + int port; +}; + +static void *session_forward_w(void *params) +{ + struct session_forward *sf = (struct session_forward *)params; + struct channel *c = sf->c; + + LIBSSH2_SESSION *session = rblibssh2_session_get(c->s); + LIBSSH2_LISTENER *listener = + libssh2_channel_forward_listen_ex(session, + "127.0.0.1", + sf->port, &sf->port, 1); + c->channel = libssh2_channel_forward_accept(listener); + if (c->channel == NULL) { + char *err; + int ssh_errno = libssh2_session_last_error(session, &err, NULL, 0); + rblibssh2_session_set_error(rb_eIOError, + "Failed to open channel: %s(%i)", err, ssh_errno); + return NULL; + } + + return c; +} + +static VALUE session_forward(VALUE self_rv, VALUE port_rv) +{ + struct session *s; + Data_Get_Struct(self_rv, struct session, s); + + if (rblibssh2_session_get(s) == NULL) + rb_raise(eInternalError, "Session is closed"); + + struct channel *c; + VALUE channel_rv = Data_Make_Struct(cChannel, struct channel, + NULL, channel_free, c); + memset(c, 0, sizeof(*c)); + c->s = s; + c->rv = channel_rv; + + rblibssh2_session_channel_add(s, channel_rv); + + struct session_forward p = { + .c = c, + .port = NUM2INT(port_rv) // TODO allow port to be nil for autoassignment + }; + + rblibssh2_session_runthread(s, session_forward_w, &p, NULL, NULL, NULL, NULL); + + return channel_rv; +} + struct channel_data { struct channel *c; char *data; @@ -427,6 +485,79 @@ static VALUE channel_send_data(VALUE self_rv, VALUE io_rv) return Qnil; } +struct channel_forward_data { + struct channel *c; + int fd; +}; + +static void *channel_forward_w(void *params) +{ + ssize_t l; + struct channel_forward_data *cfd = (struct channel_forward_data *) params; + struct channel *c = cfd->c; + + LIBSSH2_SESSION *session = rblibssh2_session_get(c->s); + + char *data = xmalloc(1024); + struct channel_data *cd = xmalloc(sizeof *cd); + cd->c = c; + cd->len = 1024; + cd->data = data; + + + while((l = libssh2_channel_read(c->channel, cd->data, cd->len)) > 0){ + if (l < 0) { + char *err; + libssh2_session_last_error(session, &err, NULL, 0); + rblibssh2_session_set_error(rb_eIOError, + "Error reading from channel in channel_read: %s(%i)", err, l); + break; + } else if (l == 0) { + if (libssh2_channel_eof(c->channel)) { + rblibssh2_session_set_error(rb_eIOError, + "Unexpected EOF on channel in channel_read"); + break; + } + /* Can't think of any other reason we'd get a zero-length return, + * but go round again anyway. */ + } else { + // send to remote socket + // TODO analyze return value + write(cfd->fd, cd->data, l); + cd->len = l; + } + } + + channel_data_free(cd); + return cfd; +} + +static VALUE channel_forward(VALUE self_rv, VALUE io_rv) +{ + struct channel *c; + Data_Get_Struct(self_rv, typeof(*c), c); + + if (c->channel == NULL) + rb_raise(eInternalError, "Channel is closed"); + + rb_io_t *io; + GetOpenFile(io_rv, io); + rb_io_check_writable(io); + + struct channel_forward_data *cfd = xmalloc(sizeof(*cfd)); + cfd->c = c; +#ifdef HAVE_STRUCT_RB_IO_T_F + cfd->fd = fileno(io->f); +#elif HAVE_STRUCT_RB_IO_T_FD + cfd->fd = io->fd; +#endif + + rblibssh2_session_runthread(c->s, channel_forward_w, + cfd, xfree, + NULL, NULL, NULL); + return Qnil; +} + static void *channel_close_w(void *arg) { struct channel *c = (struct channel *) arg; diff --git a/p2v/client/lib/virt-p2v/blockdevice.rb b/p2v/client/lib/virt-p2v/blockdevice.rb index 644d390..d0b56db 100644 --- a/p2v/client/lib/virt-p2v/blockdevice.rb +++ b/p2v/client/lib/virt-p2v/blockdevice.rb @@ -20,7 +20,8 @@ class NoSuchDeviceError < StandardError; end class InvalidDevice < StandardError; end class FixedBlockDevice - @@devices = {} + @@current_port = 1024 + @@devices = {} def self.all_devices @@devices.values @@ -32,7 +33,7 @@ class FixedBlockDevice @@devices[device] end - attr_reader :device, :size + attr_reader :device, :size, :handle def initialize(device) size = 0 @@ -55,6 +56,14 @@ class FixedBlockDevice @size = size @@devices[@device] = self end + + # export the disk using qemu-nbd + def export + @@current_port += 1 + @port = @@current_port + `/usr/bin/qemu-nbd --read-only --partition=1 --snapshot #{@device} --port #{@port}` + @handle = TCPSocket.open('localhost', @port) + end end class RemovableBlockDevice diff --git a/p2v/client/lib/virt-p2v/connection.rb b/p2v/client/lib/virt-p2v/connection.rb index c3e5537..6f7dcb6 100644 --- a/p2v/client/lib/virt-p2v/connection.rb +++ b/p2v/client/lib/virt-p2v/connection.rb @@ -243,6 +243,22 @@ class Connection } end + def import(dev, &cb) + raise NotConnectedError if @channel.nil? + + # instruct ssh to setup forwarding + channel = @session.forward_port dev.port + channel.forward dev.handle + + run(completion) { + channel.write("IMPORT #{dev.path} #{dev.port}\n") + result = parse_return + + Gtk.queue { cb.call(result) } + } + end + + private def run(cb) diff --git a/p2v/client/lib/virt-p2v/converter.rb b/p2v/client/lib/virt-p2v/converter.rb index 7cb4e33..4748d37 100644 --- a/p2v/client/lib/virt-p2v/converter.rb +++ b/p2v/client/lib/virt-p2v/converter.rb @@ -37,6 +37,10 @@ module VirtP2V # path Detected # is_block 1 # format raw +# exports Editable, default to none +# device Detected +# uri Detected +# format raw # removables Editable, default to all # device Detected # type Detected @@ -47,7 +51,7 @@ module VirtP2V class Converter attr_accessor :profile, :name, :cpus, :memory, :arch, :debug - attr_reader :features, :disks, :removables, :nics + attr_reader :features, :disks, :exports, :removables, :nics attr_reader :connection @@ -71,7 +75,14 @@ class Converter lambda { |cb2| disk(dev, status, progress, cb2) } - }, cb) + }, cb) unless @disks.empty? + }, + lambda { |cb| + iterate(@exports.map { |dev| + lambda { |cb2| + export(dev, status, cb2) + } + }, cb) unless @exports.empty? }, lambda { |cb| if @debug @@ -140,6 +151,7 @@ class Converter # Initialise empty lists for optional devices. These will be added # according to the user's selection @disks = [] + @exports = [] @removables = [] @nics = [] @@ -174,6 +186,16 @@ class Converter ], completion) end + def export(dev, status, completion) + path = "/dev/#{dev}" + status.call("Exporting #{dev}") + dev.export + iterate([ + # instruct server to import disk + lambda { |cb| @connection.import(dev, &cb) } + ], completion) + end + def iterate(stages, completion) i = 0 cb = lambda { |result| @@ -204,6 +226,12 @@ class Converter 'path' => "/dev/#{device}" } }, + 'exports' => @exports.map { |device| + { + 'device' => device, + 'path' => "/dev/#{device}", + } + }, 'removables' => @removables.map { |device| removable = RemovableBlockDevice[device] { diff --git a/p2v/client/lib/virt-p2v/ui/convert.rb b/p2v/client/lib/virt-p2v/ui/convert.rb index 244125e..acfbca2 100644 --- a/p2v/client/lib/virt-p2v/ui/convert.rb +++ b/p2v/client/lib/virt-p2v/ui/convert.rb @@ -26,9 +26,10 @@ module VirtP2V::UI::Convert CONVERT_NETWORK_DEVICE = 1 CONVERT_FIXED_CONVERT = 0 - CONVERT_FIXED_DEVICE = 1 - CONVERT_FIXED_PROGRESS = 2 - CONVERT_FIXED_SIZE_GB = 3 + CONVERT_FIXED_EXPORT = 1 + CONVERT_FIXED_DEVICE = 2 + CONVERT_FIXED_PROGRESS = 3 + CONVERT_FIXED_SIZE_GB = 4 CONVERT_REMOVABLE_CONVERT = 0 CONVERT_REMOVABLE_DEVICE = 1 @@ -99,6 +100,7 @@ module VirtP2V::UI::Convert VirtP2V::FixedBlockDevice.all_devices.each { |dev| fixed = @fixeds.append fixed[CONVERT_FIXED_CONVERT] = true + fixed[CONVERT_FIXED_EXPORT] = false fixed[CONVERT_FIXED_DEVICE] = dev.device fixed[CONVERT_FIXED_PROGRESS] = 0 fixed[CONVERT_FIXED_SIZE_GB] = dev.size / 1024 / 1024 / 1024 @@ -134,6 +136,8 @@ module VirtP2V::UI::Convert method(:update_values)) ui.register_handler('convert_fixed_select_toggled', method(:convert_fixed_select_toggled)) + ui.register_handler('export_fixed_select_toggled', + method(:export_fixed_select_toggled)) ui.register_handler('convert_removable_select_toggled', method(:convert_removable_select_toggled)) ui.register_handler('convert_network_select_toggled', @@ -319,16 +323,22 @@ module VirtP2V::UI::Convert raise InvalidUIState if memory.nil? @converter.memory = memory * 1024 * 1024 - # Check that at least 1 fixed storage device is selected - fixed = false + # TODO add option to toggle exporting disks using nbd or iscsi + + # Check that at least 1 fixed or exported storage device is selected + has_disk = false + @converter.exports.clear @converter.disks.clear @fixeds.each { |model, path, iter| if iter[CONVERT_FIXED_CONVERT] then - fixed = true + has_disk = true @converter.disks << iter[CONVERT_FIXED_DEVICE] + elsif iter[CONVERT_FIXED_EXPORT] then + has_disk = true + @converter.exports << iter[CONVERT_FIXED_EXPORT] end } - raise InvalidUIState unless fixed + raise InvalidUIState unless has_disk # Populate removables and nics, although these aren't required to be # selected for the ui state to be valid @@ -373,17 +383,16 @@ module VirtP2V::UI::Convert memory = Integer(memory) rescue nil return false if memory.nil? - # Check that at least 1 fixed storage device is selected - fixed = false + # Check that at least 1 fixed or exported storage device is selected + has_disk = false @fixeds.each { |model, path, iter| - if iter[CONVERT_FIXED_CONVERT] then - fixed = true + if iter[CONVERT_FIXED_CONVERT] || + iter[CONVERT_FIXED_EXPORT] then + has_disk = true break end } - return false unless fixed - - return true + return has_disk end def self.convert_cpus_changed @@ -416,6 +425,13 @@ module VirtP2V::UI::Convert def self.convert_fixed_select_toggled(widget, path) iter = @fixeds.get_iter(path) iter[CONVERT_FIXED_CONVERT] = !iter[CONVERT_FIXED_CONVERT] + iter[CONVERT_FIXED_EXPORT] = false if iter[CONVERT_FIXED_CONVERT] + end + + def self.export_fixed_select_toggled(widget, path) + iter = @fixeds.get_iter(path) + iter[CONVERT_FIXED_EXPORT] = !iter[CONVERT_FIXED_EXPORT] + iter[CONVERT_FIXED_CONVERT] = false if iter[CONVERT_FIXED_EXPORT] end def self.convert_removable_select_toggled(widget, path) diff --git a/p2v/client/lib/virt-p2v/ui/p2v.ui b/p2v/client/lib/virt-p2v/ui/p2v.ui index a27bd20..b5d74a2 100644 --- a/p2v/client/lib/virt-p2v/ui/p2v.ui +++ b/p2v/client/lib/virt-p2v/ui/p2v.ui @@ -6,6 +6,8 @@ <columns> <!-- column-name Convert --> <column type="gboolean"/> + <!-- column-name Export --> + <column type="gboolean"/> <!-- column-name Device --> <column type="gchararray"/> <!-- column-name Progress --> @@ -276,12 +278,26 @@ </object> </child> <child> + <object class="GtkTreeViewColumn" id="treeviewcolumn13"> + <property name="title">Export</property> + <property name="clickable">True</property> + <child> + <object class="GtkCellRendererToggle" id="export_fixed_select"> + <signal name="toggled" handler="export_fixed_select_toggled" swapped="no"/> + </object> + <attributes> + <attribute name="active">1</attribute> + </attributes> + </child> + </object> + </child> + <child> <object class="GtkTreeViewColumn" id="treeviewcolumn5"> <property name="title">Device</property> <child> <object class="GtkCellRendererText" id="cellrenderertext4"/> <attributes> - <attribute name="text">1</attribute> + <attribute name="text">2</attribute> </attributes> </child> </object> @@ -292,7 +308,7 @@ <child> <object class="GtkCellRendererText" id="cellrenderertext9"/> <attributes> - <attribute name="text">3</attribute> + <attribute name="text">4</attribute> </attributes> </child> </object> @@ -304,7 +320,7 @@ <child> <object class="GtkCellRendererProgress" id="cellrendererprogress1"/> <attributes> - <attribute name="value">2</attribute> + <attribute name="value">3</attribute> </attributes> </child> </object> diff --git a/p2v/server/virt-p2v-server.pl b/p2v/server/virt-p2v-server.pl index c15efb2..5c5b415 100755 --- a/p2v/server/virt-p2v-server.pl +++ b/p2v/server/virt-p2v-server.pl @@ -58,6 +58,7 @@ $SIG{'PIPE'} = 'IGNORE'; use constant MSG_OPTIONS => 'OPTIONS'; use constant MSG_METADATA => 'METADATA'; use constant MSG_PATH => 'PATH'; +use constant MSG_IMPORT => 'IMPORT'; use constant MSG_CONVERT => 'CONVERT'; use constant MSG_LIST_PROFILES => 'LIST_PROFILES'; use constant MSG_SET_PROFILE => 'SET_PROFILE'; @@ -68,6 +69,7 @@ my @MSGS = ( MSG_METADATA, MSG_OPTIONS, MSG_PATH, + MSG_IMPORT, MSG_CONVERT, MSG_LIST_PROFILES, MSG_SET_PROFILE, @@ -181,6 +183,13 @@ eval { receive_path($path, $length); } + # IMPORT path port + elsif ($type eq MSG_IMPORT) { + my $path = $msg->{args}[1]; + my $port = $msg->{args}[1]; + import_disk($path, $port); + } + # CONVERT elsif ($type eq MSG_CONVERT) { convert(); @@ -358,6 +367,12 @@ sub convert $_->{dst}->get_path(), $_->{dst}->get_format() ] } @{$meta->{disks}}; + foreach my $export ($meta->{exports}){ + push(@disks, map { [ $_->{device}, + "nbd://localhost:$_->{port}", + $_->{format} ] } @{$export}) + } + $g = new Sys::VirtConvert::GuestfsHandle( \@disks, $transferiso, @@ -411,6 +426,22 @@ sub convert p2v_return_ok(); } +sub import_disk +{ + my ($path,$port) = @_; + + die("PATH without prior SET_PROFILE command\n") + unless defined($target); + die("IMPORT without prior METADATA command\n") + unless defined($meta); + + my ($disk) = grep { $_->{path} eq $path } @{$meta->{exports}}; + $disk->{port} = $port; + $disk->{format} = 'raw'; + + p2v_return_ok(); +} + sub unexpected_msg { my $msg = shift; -- 1.7.11.7