Initial commands implementation

This commit is contained in:
Michael Vines 2019-03-20 16:12:50 -07:00
parent 4e029d81a2
commit f286bbac99
7 changed files with 693 additions and 93 deletions

View File

@ -8,20 +8,29 @@ repository = "https://github.com/solana-labs/solana"
license = "Apache-2.0"
homepage = "https://solana.com/"
[features]
cuda = []
[dependencies]
bincode = "1.1.2"
bs58 = "0.2.0"
clap = { version = "2.32.0"}
bzip2 = "0.3.3"
console = "0.7.5"
chrono = { version = "0.4.0", features = ["serde"] }
clap = { version = "2.32.0"}
dirs = "1.0.5"
indicatif = "0.11.0"
lazy_static = "1.3.0"
log = "0.4.2"
reqwest = "0.9.11"
ring = "0.13.2"
serde = "1.0.89"
serde_derive = "1.0.89"
serde_yaml = "0.8.8"
solana-client = { path = "../client", version = "0.13.0" }
solana-config-api = { path = "../programs/config_api", version = "0.13.0" }
solana-logger = { path = "../logger", version = "0.13.0" }
solana-sdk = { path = "../sdk", version = "0.13.0" }
[features]
cuda = []
tar = "0.4.22"
tempdir = "0.3.7"
url = "1.7.2"

View File

@ -1,57 +1,481 @@
use crate::config::Config;
use crate::update_manifest::{SignedUpdateManifest, UpdateManifest};
use chrono::{Local, TimeZone};
use console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle};
use ring::digest::{Context, Digest, SHA256};
use solana_client::rpc_client::RpcClient;
use solana_config_api::ConfigInstruction;
use solana_sdk::pubkey::Pubkey;
use std::time::Duration;
use solana_sdk::signature::{read_keypair, Keypair, KeypairUtil, Signable};
use solana_sdk::transaction::Transaction;
use std::fs::{self, File};
use std::io::{self, BufReader, Read};
use std::path::{Path, PathBuf};
use std::thread::sleep;
use std::time::SystemTime;
use std::time::{Duration, Instant};
use tempdir::TempDir;
use url::Url;
static TRUCK: Emoji = Emoji("🚚 ", "");
static LOOKING_GLASS: Emoji = Emoji("🔍 ", "");
static BULLET: Emoji = Emoji("", "* ");
static SPARKLE: Emoji = Emoji("", "");
static PACKAGE: Emoji = Emoji("📦 ", "");
/// Creates a new process bar for processing that will take an unknown amount of time
fn new_spinner_progress_bar() -> ProgressBar {
let progress_bar = ProgressBar::new(42);
progress_bar
.set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}"));
progress_bar.enable_steady_tick(100);
progress_bar
}
/// Pretty print a "name value"
fn println_name_value(name: &str, value: &str) {
println!("{} {}", style(name).bold(), value);
}
/// Downloads the release archive at `url` to a temporary location. If `expected_sha256` is
/// Some(_), produce an error if the release SHA256 doesn't match.
///
/// Returns a tuple consisting of:
/// * TempDir - drop this value to clean up the temporary location
/// * PathBuf - path to the downloaded release (within `TempDir`)
/// * String - SHA256 of the release
///
fn download_to_temp_archive(
url: &str,
expected_sha256: Option<&str>,
) -> Result<(TempDir, PathBuf, String), Box<dyn std::error::Error>> {
fn sha256_digest<R: Read>(mut reader: R) -> Result<Digest, Box<dyn std::error::Error>> {
let mut context = Context::new(&SHA256);
let mut buffer = [0; 1024];
loop {
let count = reader.read(&mut buffer)?;
if count == 0 {
break;
}
context.update(&buffer[..count]);
}
Ok(context.finish())
}
fn sha256_file_digest<P: AsRef<Path>>(path: P) -> Result<Digest, Box<dyn std::error::Error>> {
let input = File::open(path)?;
let reader = BufReader::new(input);
sha256_digest(reader)
}
let url = Url::parse(url).map_err(|err| format!("Unable to parse {}: {}", url, err))?;
let temp_dir = TempDir::new(clap::crate_name!())?;
let temp_file = temp_dir.path().join("release.tar.bz2");
let client = reqwest::Client::new();
let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(&format!("{}Downloading...", TRUCK));
let response = client.get(url).send()?;
let download_size = {
response
.headers()
.get(reqwest::header::CONTENT_LENGTH)
.and_then(|content_length| content_length.to_str().ok())
.and_then(|content_length| content_length.parse().ok())
.unwrap_or(0)
};
progress_bar.set_length(download_size);
progress_bar.set_style(
ProgressStyle::default_bar()
.template(&format!(
"{}{}{}",
"{spinner:.green} ",
TRUCK,
"Downloading [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})"
))
.progress_chars("=> "),
);
struct DownloadProgress<R> {
progress_bar: ProgressBar,
response: R,
}
impl<R: Read> Read for DownloadProgress<R> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.response.read(buf).map(|n| {
self.progress_bar.inc(n as u64);
n
})
}
}
let mut source = DownloadProgress {
progress_bar,
response,
};
let mut file = File::create(&temp_file)?;
std::io::copy(&mut source, &mut file)?;
let temp_file_sha256 = sha256_file_digest(&temp_file)
.map_err(|err| format!("Unable to hash {:?}: {}", temp_file, err))?;
let temp_file_sha256 = bs58::encode(temp_file_sha256).into_string();
if expected_sha256.is_some() && expected_sha256 != Some(&temp_file_sha256) {
Err(io::Error::new(io::ErrorKind::Other, "Incorrect hash"))?;
}
Ok((temp_dir, temp_file, temp_file_sha256))
}
/// Extracts the release archive into the specified directory
fn extract_release_archive(
archive: &Path,
extract_dir: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
use bzip2::bufread::BzDecoder;
use tar::Archive;
let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(&format!("{}Extracting...", PACKAGE));
let _ = fs::remove_dir_all(extract_dir);
fs::create_dir_all(extract_dir)?;
let tar_bz2 = File::open(archive)?;
let tar = BzDecoder::new(BufReader::new(tar_bz2));
let mut release = Archive::new(tar);
release.unpack(extract_dir)?;
progress_bar.finish_and_clear();
Ok(())
}
/// Reads the supported TARGET triple for the given release
fn load_release_target(release_dir: &Path) -> Result<String, Box<dyn std::error::Error>> {
use serde_derive::Deserialize;
#[derive(Deserialize, Debug)]
pub struct ReleaseVersion {
pub target: String,
pub commit: String,
channel: String,
}
let mut version_yml = PathBuf::from(release_dir);
version_yml.push("solana-release");
version_yml.push("version.yml");
let file = File::open(&version_yml)?;
let version: ReleaseVersion = serde_yaml::from_reader(file)?;
Ok(version.target)
}
/// Time in seconds since the UNIX_EPOCH
fn timestamp_secs() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs()
}
/// Create an empty update manifest for the given `update_manifest_keypair` if it doesn't already
/// exist on the cluster
fn new_update_manifest(
rpc_client: &RpcClient,
from_keypair: &Keypair,
update_manifest_keypair: &Keypair,
) -> Result<(), Box<dyn std::error::Error>> {
if rpc_client
.get_account_data(&update_manifest_keypair.pubkey())
.is_err()
{
let recect_blockhash = rpc_client.get_recent_blockhash()?;
let new_account = ConfigInstruction::new_account::<SignedUpdateManifest>(
&from_keypair.pubkey(),
&update_manifest_keypair.pubkey(),
1, // lamports
);
let mut transaction = Transaction::new(vec![new_account]);
transaction.sign(&[from_keypair], recect_blockhash);
rpc_client.send_and_confirm_transaction(&mut transaction, from_keypair)?;
}
Ok(())
}
/// Update the update manifest on the cluster with new content
fn store_update_manifest(
rpc_client: &RpcClient,
from_keypair: &Keypair,
update_manifest_keypair: &Keypair,
update_manifest: &SignedUpdateManifest,
) -> Result<(), Box<dyn std::error::Error>> {
let recect_blockhash = rpc_client.get_recent_blockhash()?;
let new_store = ConfigInstruction::new_store::<SignedUpdateManifest>(
&from_keypair.pubkey(),
&update_manifest_keypair.pubkey(),
update_manifest,
);
let mut transaction = Transaction::new(vec![new_store]);
transaction.sign(&[from_keypair, update_manifest_keypair], recect_blockhash);
rpc_client.send_and_confirm_transaction(&mut transaction, from_keypair)?;
Ok(())
}
/// Read the current contents of the update manifest from the cluster
fn get_update_manifest(
rpc_client: &RpcClient,
update_manifest_pubkey: &Pubkey,
) -> Result<UpdateManifest, String> {
let data = rpc_client
.get_account_data(update_manifest_pubkey)
.map_err(|err| format!("Unable to fetch update manifest: {}", err))?;
let signed_update_manifest =
SignedUpdateManifest::deserialize(update_manifest_pubkey, &data)
.map_err(|err| format!("Unable to deserialize update manifest: {}", err))?;
Ok(signed_update_manifest.manifest)
}
/// Bug the user if bin_dir is not in their PATH
fn check_env_path_for_bin_dir(config: &Config) {
use std::env;
let found = match env::var_os("PATH") {
Some(paths) => env::split_paths(&paths).any(|path| {
if let Ok(path) = path.canonicalize() {
if path == *config.bin_dir() {
return true;
}
}
false
}),
None => false,
};
if !found {
println!(
"\nPlease update your PATH environment variable to include the solana programs:\n PATH=\"{}:$PATH\"\n",
config.bin_dir().to_str().unwrap()
);
}
}
pub fn init(
config_file: &str,
data_dir: &str,
json_rpc_url: &str,
update_pubkey: &Pubkey,
update_manifest_pubkey: &Pubkey,
) -> Result<(), String> {
println!("init {:?} {:?}", json_rpc_url, update_pubkey);
Config {
data_dir: data_dir.to_string(),
json_rpc_url: json_rpc_url.to_string(),
update_pubkey: *update_pubkey,
current_install: None,
}
.save(config_file)?;
Err("Not implemented".to_string())
let config = Config::new(data_dir, json_rpc_url, update_manifest_pubkey);
config.save(config_file)?;
update(config_file)?;
Ok(())
}
pub fn info(config_file: &str, local_info_only: bool) -> Result<(), String> {
pub fn info(config_file: &str, local_info_only: bool) -> Result<Option<UpdateManifest>, String> {
let config = Config::load(config_file)?;
println!("config dir: {:?}", config_file);
println!("configuration: {:?}", config);
if local_info_only {
return Ok(());
println_name_value("JSON RPC URL:", &config.json_rpc_url);
println_name_value(
"Update manifest pubkey:",
&config.update_manifest_pubkey.to_string(),
);
fn print_update_manifest(update_manifest: &UpdateManifest) {
let when = Local.timestamp(update_manifest.timestamp_secs as i64, 0);
println_name_value(
&format!("{}release date", BULLET),
&when.format("%c").to_string(),
);
println_name_value(
&format!("{}download URL", BULLET),
&update_manifest.download_url,
);
}
// TODO: fetch info about current update manifest from the cluster
match config.current_update_manifest {
Some(ref update_manifest) => {
println_name_value("Installed version:", "");
print_update_manifest(&update_manifest);
}
None => {
println_name_value("Installed version:", "None");
}
}
Err("Not implemented".to_string())
if local_info_only {
Ok(None)
} else {
let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(&format!("{}Checking for updates...", LOOKING_GLASS));
let rpc_client = RpcClient::new(config.json_rpc_url.clone());
let manifest = get_update_manifest(&rpc_client, &config.update_manifest_pubkey)?;
progress_bar.finish_and_clear();
if Some(&manifest) == config.current_update_manifest.as_ref() {
println!("\n{}", style("Installation is up to date").italic());
Ok(None)
} else {
println!("\n{}", style("An update is available:").bold());
print_update_manifest(&manifest);
Ok(Some(manifest))
}
}
}
pub fn deploy(
config_file: &str,
json_rpc_url: &str,
from_keypair_file: &str,
download_url: &str,
update_manifest_keypair: &str,
update_manifest_keypair_file: &str,
) -> Result<(), String> {
println!("deploy {:?} {:?}", download_url, update_manifest_keypair);
let _config = Config::load(config_file)?;
Err("Not implemented".to_string())
let from_keypair = read_keypair(from_keypair_file)
.map_err(|err| format!("Unable to read {}: {}", from_keypair_file, err))?;
let update_manifest_keypair = read_keypair(update_manifest_keypair_file)
.map_err(|err| format!("Unable to read {}: {}", update_manifest_keypair_file, err))?;
// Download the release
let (temp_dir, temp_archive, temp_archive_sha256) =
download_to_temp_archive(download_url, None)
.map_err(|err| format!("Unable to download {}: {}", download_url, err))?;
// Extract it and load the release version metadata
let temp_release_dir = temp_dir.path().join("archive");
extract_release_archive(&temp_archive, &temp_release_dir).map_err(|err| {
format!(
"Unable to extract {:?} into {:?}: {}",
temp_archive, temp_release_dir, err
)
})?;
let release_target = load_release_target(&temp_release_dir).map_err(|err| {
format!(
"Unable to load release target from {:?}: {}",
temp_release_dir, err
)
})?;
println_name_value("JSON RPC URL:", json_rpc_url);
println_name_value("Update target:", &release_target);
println_name_value(
"Update manifest pubkey:",
&update_manifest_keypair.pubkey().to_string(),
);
let progress_bar = new_spinner_progress_bar();
progress_bar.set_message(&format!("{}Deploying update...", PACKAGE));
// Construct an update manifest for the release
let mut update_manifest = SignedUpdateManifest {
account_pubkey: update_manifest_keypair.pubkey(),
..SignedUpdateManifest::default()
};
update_manifest.manifest.timestamp_secs = timestamp_secs();
update_manifest.manifest.download_url = download_url.to_string();
update_manifest.manifest.download_sha256 = temp_archive_sha256;
update_manifest.sign(&update_manifest_keypair);
assert!(update_manifest.verify());
// Store the new update manifest on the cluster
let rpc_client = RpcClient::new(json_rpc_url.to_string());
new_update_manifest(&rpc_client, &from_keypair, &update_manifest_keypair)
.map_err(|err| format!("Unable to create update manifest: {}", err))?;
store_update_manifest(
&rpc_client,
&from_keypair,
&update_manifest_keypair,
&update_manifest,
)
.map_err(|err| format!("Unable to store update manifest: {:?}", err))?;
progress_bar.finish_and_clear();
println!(" {}{}", SPARKLE, style("Deployment successful").bold());
Ok(())
}
pub fn update(config_file: &str) -> Result<(), String> {
println!(
"update: BUILD_SECONDS_SINCE_UNIX_EPOCH={:?}",
Duration::from_secs(
u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap()
pub fn update(config_file: &str) -> Result<bool, String> {
let update_manifest = info(config_file, false)?;
if update_manifest.is_none() {
return Ok(false);
}
let update_manifest = update_manifest.unwrap();
if timestamp_secs()
< u64::from_str_radix(crate::build_env::BUILD_SECONDS_SINCE_UNIX_EPOCH, 10).unwrap()
{
Err("Unable to update as system time seems unreliable".to_string())?
}
let mut config = Config::load(config_file)?;
if let Some(ref current_update_manifest) = config.current_update_manifest {
if update_manifest.timestamp_secs < current_update_manifest.timestamp_secs {
Err("Unable to update to an older version".to_string())?
}
}
let (_temp_dir, temp_archive, _temp_archive_sha256) = download_to_temp_archive(
&update_manifest.download_url,
Some(&update_manifest.download_sha256),
)
.map_err(|err| {
format!(
"Unable to download {}: {}",
update_manifest.download_url, err
)
);
let _config = Config::load(config_file)?;
Err("Not implemented".to_string())
})?;
let release_dir = config.release_dir(&update_manifest.download_sha256);
extract_release_archive(&temp_archive, &release_dir).map_err(|err| {
format!(
"Unable to extract {:?} to {:?}: {}",
temp_archive, release_dir, err
)
})?;
let release_target = load_release_target(&release_dir).map_err(|err| {
format!(
"Unable to load release target from {:?}: {}",
release_dir, err
)
})?;
if release_target != crate::build_env::TARGET {
Err(format!("Incompatible update target: {}", release_target))?;
}
let _ = fs::remove_dir_all(config.bin_dir());
std::os::unix::fs::symlink(
release_dir.join("solana-release").join("bin"),
config.bin_dir(),
)
.map_err(|err| {
format!(
"Unable to symlink {:?} to {:?}: {}",
release_dir,
config.bin_dir(),
err
)
})?;
config.current_update_manifest = Some(update_manifest);
config.save(config_file)?;
check_env_path_for_bin_dir(&config);
println!(" {}{}", SPARKLE, style("Update successful").bold());
Ok(true)
}
pub fn run(
@ -59,7 +483,63 @@ pub fn run(
program_name: &str,
program_arguments: Vec<&str>,
) -> Result<(), String> {
println!("run {:?} {:?}", program_name, program_arguments);
let _config = Config::load(config_file)?;
Err("Not implemented".to_string())
let config = Config::load(config_file)?;
let full_program_path = config.bin_dir().join(program_name);
if !full_program_path.exists() {
Err(format!(
"{} does not exist",
full_program_path.to_str().unwrap()
))?;
}
let mut child_option: Option<std::process::Child> = None;
let mut now = Instant::now();
loop {
child_option = match child_option {
Some(mut child) => match child.try_wait() {
Ok(Some(status)) => {
println_name_value(
&format!("{} exited with:", program_name),
&status.to_string(),
);
None
}
Ok(None) => Some(child),
Err(err) => {
eprintln!("Error attempting to wait for program to exit: {}", err);
None
}
},
None => {
match std::process::Command::new(&full_program_path)
.args(&program_arguments)
.spawn()
{
Ok(child) => Some(child),
Err(err) => {
eprintln!("Failed to spawn {}: {:?}", program_name, err);
None
}
}
}
};
if now.elapsed().as_secs() > config.update_poll_secs {
match update(config_file) {
Ok(true) => {
// Update successful, kill current process so it will be restart
if let Some(ref mut child) = child_option {
println!("Killing program: {:?}", child.kill());
}
}
Ok(false) => {} // No update available
Err(err) => {
eprintln!("Failed to apply update: {:?}", err);
}
};
now = Instant::now();
}
sleep(Duration::from_secs(1));
}
}

View File

@ -1,24 +1,36 @@
use crate::update_manifest::SignedUpdateManifest;
use crate::update_manifest::UpdateManifest;
use serde_derive::{Deserialize, Serialize};
use serde_yaml;
use solana_sdk::pubkey::Pubkey;
use std::fs::{create_dir_all, File};
use std::io::{Error, ErrorKind, Write};
use std::path::Path;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct Config {
pub data_dir: String,
pub json_rpc_url: String,
pub update_pubkey: Pubkey,
pub current_install: Option<SignedUpdateManifest>,
pub update_manifest_pubkey: Pubkey,
pub current_update_manifest: Option<UpdateManifest>,
pub update_poll_secs: u64,
releases_dir: PathBuf,
bin_dir: PathBuf,
}
impl Config {
fn _load(config_file: &str) -> Result<Self, Error> {
pub fn new(data_dir: &str, json_rpc_url: &str, update_manifest_pubkey: &Pubkey) -> Self {
Self {
json_rpc_url: json_rpc_url.to_string(),
update_manifest_pubkey: *update_manifest_pubkey,
current_update_manifest: None,
update_poll_secs: 60, // check for updates once a minute
releases_dir: PathBuf::from(data_dir).join("releases"),
bin_dir: PathBuf::from(data_dir).join("bin"),
}
}
fn _load(config_file: &str) -> Result<Self, io::Error> {
let file = File::open(config_file.to_string())?;
let config = serde_yaml::from_reader(file)
.map_err(|err| Error::new(ErrorKind::Other, format!("{:?}", err)))?;
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
Ok(config)
}
@ -26,9 +38,9 @@ impl Config {
Self::_load(config_file).map_err(|err| format!("Unable to load {}: {:?}", config_file, err))
}
fn _save(&self, config_file: &str) -> Result<(), Error> {
fn _save(&self, config_file: &str) -> Result<(), io::Error> {
let serialized = serde_yaml::to_string(self)
.map_err(|err| Error::new(ErrorKind::Other, format!("{:?}", err)))?;
.map_err(|err| io::Error::new(io::ErrorKind::Other, format!("{:?}", err)))?;
if let Some(outdir) = Path::new(&config_file).parent() {
create_dir_all(outdir)?;
@ -43,4 +55,12 @@ impl Config {
self._save(config_file)
.map_err(|err| format!("Unable to save {}: {:?}", config_file, err))
}
pub fn bin_dir(&self) -> &PathBuf {
&self.bin_dir
}
pub fn release_dir(&self, release_sha256: &str) -> PathBuf {
self.releases_dir.join(release_sha256)
}
}

View File

@ -2,24 +2,29 @@ pub const JSON_RPC_URL: &str = "https://api.testnet.solana.com/";
lazy_static! {
pub static ref CONFIG_FILE: Option<String> = {
dirs::config_dir().map(|mut path| {
path.push("solana");
path.push("install.yml");
dirs::home_dir().map(|mut path| {
path.extend(&[".config", "solana", "install", "config.yml"]);
path.to_str().unwrap().to_string()
})
};
pub static ref USER_KEYPAIR: Option<String> = {
dirs::home_dir().map(|mut path| {
path.extend(&[".config", "solana", "id.json"]);
path.to_str().unwrap().to_string()
})
};
pub static ref DATA_DIR: Option<String> = {
dirs::data_dir().map(|mut path| {
path.push("solana");
dirs::home_dir().map(|mut path| {
path.extend(&[".local", "share", "solana", "install"]);
path.to_str().unwrap().to_string()
})
};
}
pub fn update_pubkey(target: &str) -> Option<&str> {
pub fn update_manifest_pubkey(target: &str) -> Option<&str> {
match target {
"x86_64-apple-darwin" => Some("9XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp"), // TODO: This pubkey is invalid
"x86_64-unknown-linux-gnu" => Some("8XX329sPuskWhH4DQh6k16c87dHKhXLBZTL3Gxmve8Gp"), // TODO: This pubkey is invalid
"x86_64-apple-darwin" => None,
"x86_64-unknown-linux-gnu" => Some("EVS4V6wha5J5qzw8ZJBroMq9g6EKMg5iFWqCRrKwfo3z"), // SOLANA_INSTALL_UPDATE_MANIFEST_KEYPAIR_x86_64_unknown_linux_gnu
_ => None,
}
}

View File

@ -10,6 +10,20 @@ mod config;
mod defaults;
mod update_manifest;
fn url_validator(url: String) -> Result<(), String> {
match url::Url::parse(&url) {
Ok(_) => Ok(()),
Err(err) => Err(format!("{:?}", err)),
}
}
fn pubkey_validator(pubkey: String) -> Result<(), String> {
match pubkey.parse::<Pubkey>() {
Ok(_) => Ok(()),
Err(err) => Err(format!("{:?}", err)),
}
}
fn main() -> Result<(), String> {
solana_logger::setup();
@ -39,10 +53,11 @@ fn main() -> Result<(), String> {
.long("data_dir")
.value_name("PATH")
.takes_value(true)
.required(true)
.help("Directory to store install data");
match *defaults::DATA_DIR {
Some(ref data_dir) => arg.default_value(&data_dir),
None => arg.required(true),
None => arg,
}
})
.arg(
@ -52,23 +67,22 @@ fn main() -> Result<(), String> {
.value_name("URL")
.takes_value(true)
.default_value(defaults::JSON_RPC_URL)
.validator(url_validator)
.help("JSON RPC URL for the solana cluster"),
)
.arg({
let arg = Arg::with_name("update_pubkey")
let arg = Arg::with_name("update_manifest_pubkey")
.short("p")
.long("pubkey")
.value_name("PUBKEY")
.takes_value(true)
.validator(|value| match value.parse::<Pubkey>() {
Ok(_) => Ok(()),
Err(err) => Err(format!("{:?}", err)),
})
.required(true)
.validator(pubkey_validator)
.help("Public key of the update manifest");
match defaults::update_pubkey(build_env::TARGET) {
match defaults::update_manifest_pubkey(build_env::TARGET) {
Some(default_value) => arg.default_value(default_value),
None => arg.required(true),
None => arg,
}
}),
)
@ -89,14 +103,38 @@ fn main() -> Result<(), String> {
SubCommand::with_name("deploy")
.about("deploys a new update")
.setting(AppSettings::DisableVersion)
.arg({
let arg = Arg::with_name("from_keypair_file")
.short("k")
.long("keypair")
.value_name("PATH")
.takes_value(true)
.required(true)
.help("Keypair file of the account that funds the deployment");
match *defaults::USER_KEYPAIR {
Some(ref config_file) => arg.default_value(&config_file),
None => arg,
}
})
.arg(
Arg::with_name("json_rpc_url")
.short("u")
.long("url")
.value_name("URL")
.takes_value(true)
.default_value(defaults::JSON_RPC_URL)
.validator(url_validator)
.help("JSON RPC URL for the solana cluster"),
)
.arg(
Arg::with_name("download_url")
.index(1)
.required(true)
.validator(url_validator)
.help("URL to the solana release archive"),
)
.arg(
Arg::with_name("update_manifest_keypair")
Arg::with_name("update_manifest_keypair_file")
.index(2)
.required(true)
.help("Keypair file for the update manifest (/path/to/keypair.json)"),
@ -132,24 +170,32 @@ fn main() -> Result<(), String> {
match matches.subcommand() {
("init", Some(matches)) => {
let json_rpc_url = matches.value_of("json_rpc_url").unwrap();
let update_pubkey = matches
.value_of("update_pubkey")
let update_manifest_pubkey = matches
.value_of("update_manifest_pubkey")
.unwrap()
.parse::<Pubkey>()
.unwrap();
let data_dir = matches.value_of("data_dir").unwrap();
command::init(config_file, data_dir, json_rpc_url, &update_pubkey)
command::init(config_file, data_dir, json_rpc_url, &update_manifest_pubkey)
}
("info", Some(matches)) => {
let local_info_only = matches.is_present("local_info_only");
command::info(config_file, local_info_only)
command::info(config_file, local_info_only).map(|_| ())
}
("deploy", Some(matches)) => {
let from_keypair_file = matches.value_of("from_keypair_file").unwrap();
let json_rpc_url = matches.value_of("json_rpc_url").unwrap();
let download_url = matches.value_of("download_url").unwrap();
let update_manifest_keypair = matches.value_of("update_manifest_keypair").unwrap();
command::deploy(config_file, download_url, update_manifest_keypair)
let update_manifest_keypair_file =
matches.value_of("update_manifest_keypair_file").unwrap();
command::deploy(
json_rpc_url,
from_keypair_file,
download_url,
update_manifest_keypair_file,
)
}
("update", Some(_matches)) => command::update(config_file),
("update", Some(_matches)) => command::update(config_file).map(|_| ()),
("run", Some(matches)) => {
let program_name = matches.value_of("program_name").unwrap();
let program_arguments = matches

View File

@ -1,19 +1,61 @@
use serde_derive::{Deserialize, Serialize};
use solana_sdk::signature::Signature;
use solana_config_api::ConfigState;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Signable, Signature};
use std::error;
use std::io;
/// Information required to download and apply a given update
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct UpdateManifest {
pub target: String, // Target triple (TARGET)
pub commit: String, // git sha1 of this update, must match the commit sha1 in the release tar.bz2
pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH
pub download_url: String, // Download URL to the release tar.bz2
pub download_signature: Signature, // Signature of the release tar.bz2 file, verify with the Account public key
pub download_sha256: String, // SHA256 digest of the release tar.bz2 file
}
/// Userdata of an Update Manifest program Account.
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct SignedUpdateManifest {
pub manifest: UpdateManifest,
pub manifest_signature: Signature, // Signature of UpdateInfo, verify with the Account public key
pub manifest_signature: Signature,
#[serde(skip_serializing)]
pub account_pubkey: Pubkey,
}
impl Signable for SignedUpdateManifest {
fn pubkey(&self) -> Pubkey {
self.account_pubkey
}
fn signable_data(&self) -> Vec<u8> {
bincode::serialize(&self.manifest).expect("serialize")
}
fn get_signature(&self) -> Signature {
self.manifest_signature
}
fn set_signature(&mut self, signature: Signature) {
self.manifest_signature = signature
}
}
impl SignedUpdateManifest {
pub fn deserialize(account_pubkey: &Pubkey, input: &[u8]) -> Result<Self, Box<error::Error>> {
let mut manifest: SignedUpdateManifest = bincode::deserialize(input)?;
manifest.account_pubkey = *account_pubkey;
if !manifest.verify() {
Err(io::Error::new(
io::ErrorKind::Other,
"Manifest failed to verify",
))?;
}
Ok(manifest)
}
}
impl ConfigState for SignedUpdateManifest {
fn max_space() -> u64 {
// TODO: Use a fully populated manifest to compute a better value
// bincode::serialized_size(&Self::default()).unwrap()
256
}
}

View File

@ -67,18 +67,18 @@ use solana_sdk::signature::Signature;
/// Information required to download and apply a given update
pub struct UpdateManifest {
pub target: String, // Target triple (TARGET)
pub commit: String, // git sha1 of this update, must match the commit sha1 in the release tar.bz2
pub timestamp_secs: u64, // When the release was deployed (seconds since UNIX EPOC)
pub download_url: String, // Download URL to the release tar.bz2
pub download_signature: Signature, // Signature of the release tar.bz2 file, verify with the Account public key
pub timestamp_secs: u64, // When the release was deployed in seconds since UNIX EPOCH
pub download_url: String, // Download URL to the release tar.bz2
pub download_sha256: String, // SHA256 digest of the release tar.bz2 file
}
/// Userdata of an Update Manifest program Account.
#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
pub struct SignedUpdateManifest {
pub manifest: UpdateManifest,
pub manifest_signature: Signature, // Signature of UpdateInfo, verify with the Account public key
pub manifest: UpdateManifest,
pub manifest_signature: Signature,
}
```
Note that the `manifest` field itself contains a corresponding signature
@ -92,9 +92,8 @@ update with an older `timestamp_secs` than what is currently installed.
A release archive is expected to be a tar file compressed with
bzip2 with the following internal structure:
* `/version.yml` - a simple YAML file containing the fields (1) `"commit"` - the git
sha1 for this release, and (2) `"target"` - the target tuple. Any additional
fields are ignored.
* `/version.yml` - a simple YAML file containing the field `"target"` - the
target tuple. Any additional fields are ignored.
* `/bin/` -- directory containing available programs in the release.
`solana-install` will symlink this directory to
`~/.local/share/solana-install/bin` for use by the `PATH` environment
@ -106,10 +105,9 @@ bzip2 with the following internal structure:
The `solana-install` tool is used by the user to install and update their cluster software.
It manages the following files and directories in the user's home directory:
* `~/.config/solana/updater.json` - user configuration and information about currently installed software version
* `~/.local/share/solana-install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/<update-pubkey>-<manifest_signature>/bin`
* `~/.local/share/solana-install/<update-pubkey>-<manifest_signature>/` - contents of the release
* `~/.local/share/solana-install/<update-pubkey>-<manifest_signature>.tmp/` - temporary directory used while downloading a new release
* `~/.config/solana/install/config.yml` - user configuration and information about currently installed software version
* `~/.local/share/solana/install/bin` - a symlink to the current release. eg, `~/.local/share/solana-update/<update-pubkey>-<manifest_signature>/bin`
* `~/.local/share/solana/install/releases/<download_sha256>/` - contents of a release
#### Command-line Interface