Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions e2e/tests-dfx/canister_migration.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bats

load ../utils/_

setup() {
standard_setup
dfx_new hello
}

teardown() {
dfx_stop
standard_teardown
}

@test "canister migrate canister id" {
dfx_start --system-canisters
install_asset counter

# Update dfx.json: rename hello_backend -> source, and add target canister
jq '.canisters.source = .canisters.hello_backend | del(.canisters.hello_backend)' dfx.json | sponge dfx.json
jq '.canisters.target = { "main": "counter.mo", "type": "motoko" }' dfx.json | sponge dfx.json

# Deploy the source to the application subnet.
dfx deploy source

# Create the target canister on the fiduciary subnet.
dfx canister create target --subnet-type fiduciary

dfx canister stop source
dfx canister stop target

# Make sure the source has enough cycles to do the migration.
dfx ledger fabricate-cycles --canister source --cycles 10000000000000

# The migration will take a few minutes to complete.
assert_command dfx canister migrate-id source --replace target --yes
assert_contains "Migration succeeded"

assert_command dfx canister status source
assert_command_fail dfx canister status target
}
2 changes: 1 addition & 1 deletion src/dfx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ os_str_bytes = { version = "6.3.0", features = ["conversions"] }
patch = "0.7.0"
pem.workspace = true
petgraph = "0.6.0"
pocket-ic = { git = "https://github.com/dfinity/ic", rev = "575bcd0954e9d00066fd465223b755bda645edd6" }
pocket-ic = { git = "https://github.com/dfinity/ic", rev = "9276bb4c90c5adc5718cd144342e6b56c13a3ff6" }
rand = "0.8.5"
regex = "1.5.5"
reqwest = { workspace = true, features = ["blocking", "json"] }
Expand Down
26 changes: 13 additions & 13 deletions src/dfx/assets/dfx-asset-sources.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
{
"replica-rev": "575bcd0954e9d00066fd465223b755bda645edd6",
"replica-rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6",
"x86_64-darwin": {
"motoko": {
"url": "https://github.com/dfinity/motoko/releases/download/0.16.2/motoko-Darwin-x86_64-0.16.2.tar.gz",
"sha256": "db748aeecd9be11898b57b98c497ba7c79896b7bb5d1c48e9dc27ee35b57ada0",
"version": "0.16.2"
},
"pocket-ic": {
"url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/x86_64-darwin/pocket-ic.gz",
"sha256": "aae66c74224421a9a8055b48d2189891eea12d8fa932ec5fb36a6c827e0be43a",
"rev": "575bcd0954e9d00066fd465223b755bda645edd6"
"url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/x86_64-darwin/pocket-ic.gz",
"sha256": "3af2af65b87d6a6b3c3bfb4ee08a42d840c29a881a652a1e63eeac496c029597",
"rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6"
}
},
"arm64-darwin": {
Expand All @@ -19,9 +19,9 @@
"version": "0.16.2"
},
"pocket-ic": {
"url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/arm64-darwin/pocket-ic.gz",
"sha256": "988020fde4cfba0abfd9957d4364dcab6d5c2b64aae19c786f33474a192ff11e",
"rev": "575bcd0954e9d00066fd465223b755bda645edd6"
"url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/arm64-darwin/pocket-ic.gz",
"sha256": "a9b8b29b88a47069babb18e524abdbd9f54a7a32ff878d93ba5c93230faae7de",
"rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6"
}
},
"x86_64-linux": {
Expand All @@ -31,9 +31,9 @@
"version": "0.16.2"
},
"pocket-ic": {
"url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/x86_64-linux/pocket-ic.gz",
"sha256": "99edb5b28c1e62ad3d2b86b3ad022c98ddc734671dad9abe1fc672bc0048d1e9",
"rev": "575bcd0954e9d00066fd465223b755bda645edd6"
"url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/x86_64-linux/pocket-ic.gz",
"sha256": "08e45801214ec1f0e95a483e39a007553785a5cb676c4144eb8a5eb2749bf67b",
"rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6"
}
},
"arm64-linux": {
Expand All @@ -43,9 +43,9 @@
"version": "0.16.2"
},
"pocket-ic": {
"url": "https://download.dfinity.systems/ic/575bcd0954e9d00066fd465223b755bda645edd6/binaries/arm64-linux/pocket-ic.gz",
"sha256": "bd5aae42649a68e7febd715d7cadf4a78a6b9b5e2cd5fb6ae528750a463fdd77",
"rev": "575bcd0954e9d00066fd465223b755bda645edd6"
"url": "https://download.dfinity.systems/ic/9276bb4c90c5adc5718cd144342e6b56c13a3ff6/binaries/arm64-linux/pocket-ic.gz",
"sha256": "f4db572e52cda7d92a4b6a233c240df0fa9cbd1f1e91034a97e03de75a43e375",
"rev": "9276bb4c90c5adc5718cd144342e6b56c13a3ff6"
}
},
"common": {
Expand Down
1 change: 1 addition & 0 deletions src/dfx/src/actors/pocketic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,7 @@ async fn initialize_pocketic(
ii: Some(IcpFeaturesConfig::default()),
nns_ui: Some(IcpFeaturesConfig::default()),
bitcoin: None,
canister_migration: Some(IcpFeaturesConfig::default()),
})
} else {
None
Expand Down
200 changes: 200 additions & 0 deletions src/dfx/src/commands/canister/migrate_id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::lib::ic_attributes::CanisterSettings;
use crate::lib::operations::canister::{
get_canister_status, list_canister_snapshots, update_settings,
};
use crate::lib::operations::canister_migration::{
MigrationStatus, NNS_MIGRATION_CANISTER_ID, migrate_canister, migration_status,
};
use crate::lib::root_key::fetch_root_key_if_needed;
use crate::lib::subnet::get_subnet_for_canister;
use crate::util::ask_for_consent;
use anyhow::{Context, bail};
use candid::Principal;
use clap::Parser;
use dfx_core::identity::CallSender;
use ic_management_canister_types::CanisterStatusType;
use num_traits::ToPrimitive;
use slog::{debug, error, info};
use std::time::Duration;
use time::{OffsetDateTime, macros::format_description};

/// Migrate a canister ID from one subnet to another.
#[derive(Parser)]
#[command(override_usage = "dfx canister migrate-id [OPTIONS] <CANISTER> --replace <REPLACE>")]
pub struct CanisterMigrateIdOpts {
/// Specifies the name or id of the canister to migrate.
canister: String,

/// Specifies the name or id of the canister to replace.
#[arg(long)]
replace: String,

/// Skips yes/no checks by answering 'yes'. Not recommended outside of CI.
#[arg(long, short)]
yes: bool,
}

pub async fn exec(
env: &dyn Environment,
opts: CanisterMigrateIdOpts,
call_sender: &CallSender,
) -> DfxResult {
fetch_root_key_if_needed(env).await?;

let log = env.get_logger();
let agent = env.get_agent();
let canister_id_store = env.get_canister_id_store()?;

// Get the canister IDs.
let source_canister = opts.canister.as_str();
let target_canister = opts.replace.as_str();
let source_canister_id = Principal::from_text(source_canister)
.or_else(|_| canister_id_store.get(source_canister))?;
let target_canister_id = Principal::from_text(target_canister)
.or_else(|_| canister_id_store.get(target_canister))?;

if source_canister_id == target_canister_id {
bail!("The canisters to migrate and replace are identical.");
}

if !opts.yes {
ask_for_consent(
env,
&format!("Canister '{source_canister}' will be removed from its own subnet. Continue?"),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could additionally check if the canister is uninstalled and has any data - e.g., snapshots.

)?;
}

let source_status = get_canister_status(env, source_canister_id, call_sender)
.await
.with_context(|| format!("Could not retrieve status of canister {source_canister}"))?;
let target_status = get_canister_status(env, target_canister_id, call_sender)
.await
.with_context(|| format!("Could not retrieve status of canister {target_canister}"))?;

ensure_canister_stopped(source_status.status, source_canister)?;
ensure_canister_stopped(target_status.status, target_canister)?;

// Check the cycles balance of source_canister.
let cycles = source_status
.cycles
.0
.to_u128()
.expect("Unable to parse cycles");
if cycles < 10_000_000_000_000 {
bail!("Canister '{source_canister}' has less than 10T cycles");
}
if !opts.yes && cycles > 15_000_000_000_000 {
ask_for_consent(
env,
&format!(
"Canister '{source_canister}' has more than 15T cycles. The extra cycles will get burned during the migration. Continue?"
),
)?;
}

// Check that the target canister has no snapshots.
let snapshots = list_canister_snapshots(env, target_canister_id, call_sender).await?;
if !snapshots.is_empty() {
bail!(
"The canister '{}' whose canister ID will be replaced has snapshots",
target_canister
);
}

// Check that the two canisters are on different subnets.
let source_subnet = get_subnet_for_canister(agent, source_canister_id).await?;
let target_subnet = get_subnet_for_canister(agent, target_canister_id).await?;
if source_subnet == target_subnet {
bail!("The canisters '{source_canister}' and '{target_canister}' are on the same subnet");
}

// Add the NNS migration canister as a controller to the source canister.
let mut controllers = source_status.settings.controllers.clone();
if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) {
controllers.push(NNS_MIGRATION_CANISTER_ID);
let settings = CanisterSettings {
controllers: Some(controllers),
compute_allocation: None,
memory_allocation: None,
freezing_threshold: None,
reserved_cycles_limit: None,
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
};
update_settings(env, source_canister_id, settings, call_sender).await?;
}

// Add the NNS migration canister as a controller to the target canister.
let mut controllers = target_status.settings.controllers.clone();
if !controllers.contains(&NNS_MIGRATION_CANISTER_ID) {
controllers.push(NNS_MIGRATION_CANISTER_ID);
let settings = CanisterSettings {
controllers: Some(controllers),
compute_allocation: None,
memory_allocation: None,
freezing_threshold: None,
reserved_cycles_limit: None,
wasm_memory_limit: None,
wasm_memory_threshold: None,
log_visibility: None,
environment_variables: None,
};
update_settings(env, target_canister_id, settings, call_sender).await?;
}

// Migrate the from canister to the rename_to canister.
debug!(log, "Migrate '{source_canister}' to '{target_canister}'");
migrate_canister(agent, source_canister_id, target_canister_id).await?;

// Wait for migration to complete.
let spinner = env.new_spinner("Waiting for migration to complete...".into());
loop {
let statuses = migration_status(agent, source_canister_id, target_canister_id).await?;
match statuses.first() {
Some(MigrationStatus::InProgress { status }) => {
spinner.set_message(format!("Migration in progress: {status}").into());
}
Some(MigrationStatus::Succeeded { time }) => {
spinner.finish_and_clear();
info!(log, "Migration succeeded at {}", format_time(time));
break;
}
Some(MigrationStatus::Failed { reason, time }) => {
spinner.finish_and_clear();
error!(log, "Migration failed at {}: {}", format_time(time), reason);
break;
}
None => (),
}

tokio::time::sleep(Duration::from_secs(1)).await;
}

canister_id_store.remove(log, target_canister)?;

Ok(())
}

fn ensure_canister_stopped(status: CanisterStatusType, canister: &str) -> DfxResult {
match status {
CanisterStatusType::Stopped => Ok(()),
CanisterStatusType::Running => {
bail!("Canister {canister} is running. Run 'dfx canister stop' first");
}
CanisterStatusType::Stopping => {
bail!("Canister {canister} is stopping. Wait a few seconds and try again");
}
}
}

fn format_time(time: &u64) -> String {
let format = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC");
OffsetDateTime::from_unix_timestamp_nanos(*time as i128)
.unwrap()
.format(&format)
.unwrap()
}
Loading