@@ -4,14 +4,15 @@ use std::path::{Path, PathBuf};
44use std:: {
55 net:: { IpAddr , SocketAddr , ToSocketAddrs } ,
66 os:: unix:: prelude:: AsRawFd ,
7+ time:: Duration ,
78} ;
89
910use anyhow:: { anyhow, Context } ;
10- use futures:: { SinkExt , StreamExt } ;
11+ use futures:: { future , SinkExt , StreamExt } ;
1112use propolis_client:: {
1213 api:: {
13- DiskRequest , InstanceEnsureRequest , InstanceProperties ,
14- InstanceStateRequested ,
14+ DiskRequest , InstanceEnsureRequest , InstanceMigrateInitiateRequest ,
15+ InstanceProperties , InstanceStateRequested , MigrationState ,
1516 } ,
1617 Client ,
1718} ;
@@ -50,6 +51,10 @@ enum Command {
5051 /// Instance name
5152 name : String ,
5253
54+ /// Instance uuid (if specified)
55+ #[ structopt( short = "u" ) ]
56+ uuid : Option < Uuid > ,
57+
5358 /// Number of vCPUs allocated to instance
5459 #[ structopt( short = "c" , default_value = "4" ) ]
5560 vcpus : u8 ,
@@ -84,6 +89,24 @@ enum Command {
8489 /// Instance name
8590 name : String ,
8691 } ,
92+
93+ /// Migrate instance to new propolis-server
94+ Migrate {
95+ /// Instance name
96+ name : String ,
97+
98+ /// Destination propolis-server address
99+ #[ structopt( parse( try_from_str = resolve_host) ) ]
100+ dst_server : IpAddr ,
101+
102+ /// Destination propolis-server port
103+ #[ structopt( short = "p" , default_value = "12400" ) ]
104+ dst_port : u16 ,
105+
106+ /// Uuid for the destination instance
107+ #[ structopt( short = "u" ) ]
108+ dst_uuid : Option < Uuid > ,
109+ } ,
87110}
88111
89112fn parse_state ( state : & str ) -> anyhow:: Result < InstanceStateRequested > {
@@ -128,13 +151,11 @@ fn create_logger(opt: &Opt) -> Logger {
128151async fn new_instance (
129152 client : & Client ,
130153 name : String ,
154+ id : Uuid ,
131155 vcpus : u8 ,
132156 memory : u64 ,
133157 disks : Vec < DiskRequest > ,
134158) -> anyhow:: Result < ( ) > {
135- // Generate a UUID for the new instance
136- let id = Uuid :: new_v4 ( ) ;
137-
138159 let properties = InstanceProperties {
139160 id,
140161 name,
@@ -151,7 +172,8 @@ async fn new_instance(
151172 properties,
152173 // TODO: Allow specifying NICs
153174 nics : vec ! [ ] ,
154- disks : disks. to_vec ( ) ,
175+ disks,
176+ migrate : None ,
155177 } ;
156178
157179 // Try to create the instance
@@ -261,6 +283,72 @@ async fn serial(
261283 Ok ( ( ) )
262284}
263285
286+ async fn migrate_instance (
287+ src_client : Client ,
288+ dst_client : Client ,
289+ src_name : String ,
290+ src_addr : SocketAddr ,
291+ dst_uuid : Uuid ,
292+ ) -> anyhow:: Result < ( ) > {
293+ // Grab the src instance UUID
294+ let src_uuid = src_client
295+ . instance_get_uuid ( & src_name)
296+ . await
297+ . with_context ( || anyhow ! ( "failed to get src instance UUID" ) ) ?;
298+
299+ // Grab the instance details
300+ let src_instance = src_client
301+ . instance_get ( src_uuid)
302+ . await
303+ . with_context ( || anyhow ! ( "failed to get src instance properties" ) ) ?;
304+
305+ let request = InstanceEnsureRequest {
306+ properties : InstanceProperties {
307+ // Use a new ID for the destination instance we're creating
308+ id : dst_uuid,
309+ ..src_instance. instance . properties
310+ } ,
311+ // TODO: Handle migrating NICs & disks
312+ nics : vec ! [ ] ,
313+ disks : vec ! [ ] ,
314+ migrate : Some ( InstanceMigrateInitiateRequest { src_addr, src_uuid } ) ,
315+ } ;
316+
317+ // Initiate the migration via the destination instance
318+ let migration_id = dst_client
319+ . instance_ensure ( & request)
320+ . await ?
321+ . migrate
322+ . ok_or_else ( || anyhow ! ( "no migrate id on response" ) ) ?
323+ . migration_id ;
324+
325+ // Wait for the migration to complete by polling both source and destination
326+ // TODO: replace with into_iter method call after edition upgrade
327+ let handles = IntoIterator :: into_iter ( [
328+ ( "src" , src_client, src_uuid) ,
329+ ( "dst" , dst_client, dst_uuid) ,
330+ ] )
331+ . map ( |( role, client, id) | {
332+ tokio:: spawn ( async move {
333+ loop {
334+ let state = client
335+ . instance_migrate_status ( id, migration_id)
336+ . await ?
337+ . state ;
338+ println ! ( "{}({}) migration state={:?}" , role, id, state) ;
339+ if state == MigrationState :: Finish {
340+ return Ok :: < _ , anyhow:: Error > ( ( ) ) ;
341+ }
342+ tokio:: time:: sleep ( Duration :: from_secs ( 1 ) ) . await ;
343+ }
344+ } )
345+ } ) ;
346+
347+ future:: join_all ( handles) . await ;
348+
349+ Ok ( ( ) )
350+ }
351+
264352#[ tokio:: main]
265353async fn main ( ) -> anyhow:: Result < ( ) > {
266354 let opt = Opt :: from_args ( ) ;
@@ -270,20 +358,33 @@ async fn main() -> anyhow::Result<()> {
270358 let client = Client :: new ( addr, log. new ( o ! ( ) ) ) ;
271359
272360 match opt. cmd {
273- Command :: New { name, vcpus, memory, crucible_disks } => {
361+ Command :: New { name, uuid , vcpus, memory, crucible_disks } => {
274362 let disks = if let Some ( crucible_disks) = crucible_disks {
275363 parse_crucible_disks ( & crucible_disks) ?
276364 } else {
277365 vec ! [ ]
278366 } ;
279- new_instance ( & client, name. to_string ( ) , vcpus, memory, disks)
280- . await ?
367+ new_instance (
368+ & client,
369+ name. to_string ( ) ,
370+ uuid. unwrap_or_else ( Uuid :: new_v4) ,
371+ vcpus,
372+ memory,
373+ disks,
374+ )
375+ . await ?
281376 }
282377 Command :: Get { name } => get_instance ( & client, name) . await ?,
283378 Command :: State { name, state } => {
284379 put_instance ( & client, name, state) . await ?
285380 }
286381 Command :: Serial { name } => serial ( & client, addr, name) . await ?,
382+ Command :: Migrate { name, dst_server, dst_port, dst_uuid } => {
383+ let dst_addr = SocketAddr :: new ( dst_server, dst_port) ;
384+ let dst_client = Client :: new ( dst_addr, log. clone ( ) ) ;
385+ let dst_uuid = dst_uuid. unwrap_or_else ( Uuid :: new_v4) ;
386+ migrate_instance ( client, dst_client, name, addr, dst_uuid) . await ?
387+ }
287388 }
288389
289390 Ok ( ( ) )
0 commit comments