Figure I'd share the latest revision to p2v I had regarding $subject. The patch now applies against HEAD and compiles fine. I was able to update the package on the client and run virt-p2v, though have yet to do a full end-to-end verification Most likely a little more work is needed to tidy up some edge cases and fully flush things out, but the majority of the work should be in place. On the client side the disk is exported using qemu-nbd and an new ssh forwarding channel is opened up infront of it. The server simply instructs libguestfs to connect to nbd://localhost:port/ and the rest is taken care of.
Mo Morsi
2013-May-16 20:27 UTC
[Libguestfs] [PATCH] support remotely mounting disk images in p2v
--- 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