diff --git a/Makefile b/Makefile index 6eca68ae1b..c76af04a91 100644 --- a/Makefile +++ b/Makefile @@ -16,9 +16,10 @@ COMPONENTS += runtime-rs TOOLS = TOOLS += agent-ctl -TOOLS += trace-forwarder -TOOLS += runk +TOOLS += kata-ctl TOOLS += log-parser +TOOLS += runk +TOOLS += trace-forwarder STANDARD_TARGETS = build check clean install test vendor diff --git a/README.md b/README.md index c29a1c8595..6972ed2786 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ The table below lists the remaining parts of the project: | [kernel](https://www.kernel.org) | kernel | Linux kernel used by the hypervisor to boot the guest image. Patches are stored [here](tools/packaging/kernel). | | [osbuilder](tools/osbuilder) | infrastructure | Tool to create "mini O/S" rootfs and initrd images and kernel for the hypervisor. | | [`agent-ctl`](src/tools/agent-ctl) | utility | Tool that provides low-level access for testing the agent. | +| [`kata-ctl`](src/tools/kata-ctl) | utility | Tool that provides advanced commands and debug facilities. | | [`trace-forwarder`](src/tools/trace-forwarder) | utility | Agent tracing helper. | | [`runk`](src/tools/runk) | utility | Standard OCI container runtime based on the agent. | | [`ci`](https://github.com/kata-containers/ci) | CI | Continuous Integration configuration files and scripts. | diff --git a/src/tools/kata-ctl/.gitignore b/src/tools/kata-ctl/.gitignore new file mode 100644 index 0000000000..57872d0f1e --- /dev/null +++ b/src/tools/kata-ctl/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/src/tools/kata-ctl/Cargo.toml b/src/tools/kata-ctl/Cargo.toml new file mode 100644 index 0000000000..094613c343 --- /dev/null +++ b/src/tools/kata-ctl/Cargo.toml @@ -0,0 +1,20 @@ +# Copyright (c) 2022 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +[package] +name = "kata-ctl" +version = "0.0.1" +authors = ["The Kata Containers community "] +edition = "2018" + +[dependencies] +anyhow = "1.0.31" +clap = { version = "3.2.20", features = ["derive", "cargo"] } +reqwest = { version = "0.11", default-features = false, features = ["json", "blocking", "rustls-tls"] } +serde_json = "1.0.85" +thiserror = "1.0.35" + +[dev-dependencies] +semver = "1.0.12" diff --git a/src/tools/kata-ctl/Makefile b/src/tools/kata-ctl/Makefile new file mode 100644 index 0000000000..e11af38457 --- /dev/null +++ b/src/tools/kata-ctl/Makefile @@ -0,0 +1,64 @@ +# Copyright (c) 2022 Intel Corporation +# +# SPDX-License-Identifier: Apache-2.0 +# + +include ../../../utils.mk + +PROJECT_NAME = Kata Containers +PROJECT_URL = https://github.com/kata-containers +PROJECT_COMPONENT = kata-ctl + +TARGET = $(PROJECT_COMPONENT) + +VERSION_FILE := ./VERSION +VERSION := $(shell grep -v ^\# $(VERSION_FILE)) +COMMIT_NO := $(shell git rev-parse HEAD 2>/dev/null || true) +COMMIT_NO_SHORT := $(shell git rev-parse --short HEAD 2>/dev/null || true) +COMMIT := $(if $(shell git status --porcelain --untracked-files=no 2>/dev/null || true),${COMMIT_NO}-dirty,${COMMIT_NO}) + +# Exported to allow cargo to see it +export KATA_CTL_VERSION := $(if $(COMMIT),$(VERSION)-$(COMMIT),$(VERSION)) + +GENERATED_CODE = src/ops/version.rs + +GENERATED_REPLACEMENTS= \ + KATA_CTL_VERSION + +GENERATED_FILES := $(GENERATED_CODE) + +.DEFAULT_GOAL := default + +default: $(TARGET) build + +$(TARGET): $(GENERATED_CODE) + +build: + @RUSTFLAGS="$(EXTRA_RUSTFLAGS) --deny warnings" cargo build --target $(TRIPLE) $(if $(findstring release,$(BUILD_TYPE)),--release) $(EXTRA_RUSTFEATURES) + +$(GENERATED_FILES): %: %.in + @sed $(foreach r,$(GENERATED_REPLACEMENTS),-e 's|@$r@|$($r)|g') "$<" > "$@" + + +clean: + @cargo clean + @rm -f $(GENERATED_FILES) + +vendor: + cargo vendor + +test: + @RUSTFLAGS="$(EXTRA_RUSTFLAGS) --deny warnings" cargo test --target $(TRIPLE) $(if $(findstring release,$(BUILD_TYPE)),--release) $(EXTRA_RUSTFEATURES) + +install: + @RUSTFLAGS="$(EXTRA_RUSTFLAGS) --deny warnings" cargo install --target $(TRIPLE) --path . + +check: standard_rust_check + +.PHONY: \ + build \ + check \ + clean \ + install \ + test \ + vendor diff --git a/src/tools/kata-ctl/README.md b/src/tools/kata-ctl/README.md new file mode 100644 index 0000000000..bf908f60d0 --- /dev/null +++ b/src/tools/kata-ctl/README.md @@ -0,0 +1,49 @@ +# Kata Containers control tool + +## Overview + +The `kata-ctl` tool is a rust rewrite of the +[`kata-runtime`](../../runtime/cmd/kata-runtime) +[utility program](../../../docs/design/architecture/README.md#utility-program). + +The program provides a number of utility commands for: + +- Using advanced Kata Containers features. +- Problem determination and debugging. + +## Audience and environment + +Users and administrators. + +## Build the tool + +```bash +$ make +``` + +## Install the tool + +```bash +$ make install +``` + +## Run the tool + +```bash +$ kata-ctl ... +``` + +For example, to determine if your system is capable of running Kata +Containers, run: + +```bash +$ kata-ctl check all +``` + +### Full details + +For a usage statement, run: + +```bash +$ kata-ctl --help +``` diff --git a/src/tools/kata-ctl/VERSION b/src/tools/kata-ctl/VERSION new file mode 120000 index 0000000000..d62dc733ef --- /dev/null +++ b/src/tools/kata-ctl/VERSION @@ -0,0 +1 @@ +../../../VERSION \ No newline at end of file diff --git a/src/tools/kata-ctl/src/arch/aarch64/mod.rs b/src/tools/kata-ctl/src/arch/aarch64/mod.rs new file mode 100644 index 0000000000..6df39ce748 --- /dev/null +++ b/src/tools/kata-ctl/src/arch/aarch64/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[cfg(target_arch = "aarch64")] +pub use arch_specific::*; + +mod arch_specific { + use anyhow::Result; + + pub fn check() -> Result<()> { + unimplemented!("Check not implemented in aarch64") + } +} diff --git a/src/tools/kata-ctl/src/arch/mod.rs b/src/tools/kata-ctl/src/arch/mod.rs new file mode 100644 index 0000000000..e72bcc4537 --- /dev/null +++ b/src/tools/kata-ctl/src/arch/mod.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use anyhow::Result; + +#[cfg(target_arch = "aarch64")] +pub mod aarch64; + +#[cfg(target_arch = "powerpc64le")] +pub mod powerpc64le; + +#[cfg(target_arch = "s390x")] +pub mod s390x; + +#[cfg(target_arch = "x86_64")] +pub mod x86_64; + +pub fn check() -> Result<()> { + #[cfg(target_arch = "aarch64")] + let result = aarch64::check(); + + #[cfg(target_arch = "powerpc64le")] + let result = powerpc64le::check(); + + #[cfg(target_arch = "s390x")] + let result = s390x::check(); + + #[cfg(target_arch = "x86_64")] + let result = x86_64::check(); + + #[cfg(not(any( + target_arch = "aarch64", + target_arch = "powerpc64le", + target_arch = "s390x", + target_arch = "x86_64" + )))] + compile_error!("unknown architecture"); + + result +} diff --git a/src/tools/kata-ctl/src/arch/powerpc64le/mod.rs b/src/tools/kata-ctl/src/arch/powerpc64le/mod.rs new file mode 100644 index 0000000000..a87ab02f8c --- /dev/null +++ b/src/tools/kata-ctl/src/arch/powerpc64le/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[cfg(target_arch = "powerpc64le")] +pub use arch_specific::*; + +mod arch_specific { + use anyhow::Result; + + pub fn check() -> Result<()> { + unimplemented!("Check not implemented in powerpc64le"); + } +} diff --git a/src/tools/kata-ctl/src/arch/s390x/mod.rs b/src/tools/kata-ctl/src/arch/s390x/mod.rs new file mode 100644 index 0000000000..7f6a424c3b --- /dev/null +++ b/src/tools/kata-ctl/src/arch/s390x/mod.rs @@ -0,0 +1,15 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[cfg(target_arch = "s390x")] +pub use arch_specific::*; + +mod arch_specific { + use anyhow::Result; + + pub fn check() -> Result<()> { + unimplemented!("Check not implemented in s390x"); + } +} diff --git a/src/tools/kata-ctl/src/arch/x86_64/mod.rs b/src/tools/kata-ctl/src/arch/x86_64/mod.rs new file mode 100644 index 0000000000..95817981e1 --- /dev/null +++ b/src/tools/kata-ctl/src/arch/x86_64/mod.rs @@ -0,0 +1,55 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +#[cfg(target_arch = "x86_64")] +pub use arch_specific::*; + +mod arch_specific { + use crate::check; + use anyhow::{anyhow, Result}; + + const PROC_CPUINFO: &str = "/proc/cpuinfo"; + const CPUINFO_DELIMITER: &str = "\nprocessor"; + const CPUINFO_FLAGS_TAG: &str = "flags"; + const CPU_FLAGS_INTEL: &[&str] = &["lm", "sse4_1", "vmx"]; + const CPU_ATTRIBS_INTEL: &[&str] = &["GenuineIntel"]; + + // check cpu + fn check_cpu() -> Result<()> { + println!("INFO: check CPU: x86_64"); + + let cpu_info = check::get_single_cpu_info(PROC_CPUINFO, CPUINFO_DELIMITER)?; + + let cpu_flags = check::get_cpu_flags(&cpu_info, CPUINFO_FLAGS_TAG) + .map_err(|e| anyhow!("Error parsing CPU flags, file {:?}, {:?}", PROC_CPUINFO, e))?; + + // perform checks + // TODO: Perform checks based on hypervisor type + // TODO: Add more information to output (see kata-check in go tool); adjust formatting + let missing_cpu_attributes = check::check_cpu_attribs(&cpu_info, CPU_ATTRIBS_INTEL)?; + if !missing_cpu_attributes.is_empty() { + eprintln!( + "WARNING: Missing CPU attributes {:?}", + missing_cpu_attributes + ); + } + let missing_cpu_flags = check::check_cpu_flags(&cpu_flags, CPU_FLAGS_INTEL)?; + if !missing_cpu_flags.is_empty() { + eprintln!("WARNING: Missing CPU flags {:?}", missing_cpu_flags); + } + + Ok(()) + } + + pub fn check() -> Result<()> { + println!("INFO: check: x86_64"); + + let _cpu_result = check_cpu(); + + // TODO: collect outcome of tests to determine if checks pass or not + + Ok(()) + } +} diff --git a/src/tools/kata-ctl/src/args.rs b/src/tools/kata-ctl/src/args.rs new file mode 100644 index 0000000000..449bcb5e9d --- /dev/null +++ b/src/tools/kata-ctl/src/args.rs @@ -0,0 +1,86 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use clap::{Args, Parser, Subcommand}; + +use thiserror::Error; + +#[derive(Parser, Debug)] +#[clap(name = "kata-ctl", author, about = "Kata Containers control tool")] +pub struct KataCtlCli { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + /// Tests if system can run Kata Containers + Check(CheckArgument), + + /// Directly assign a volume to Kata Containers to manage + DirectVolume, + + /// Display settings + Env, + + /// Enter into guest by debug console + Exec, + + /// Manage vm factory + Factory, + + /// Manages iptables + Iptables(IptablesCommand), + + /// Gather metrics associated with infrastructure used to run a sandbox + Metrics(MetricsCommand), + + /// Display version details + Version, +} + +#[derive(Debug, Args, Error)] +#[error("Argument is not valid")] +pub struct CheckArgument { + #[clap(subcommand)] + pub command: CheckSubCommand, +} + +#[derive(Debug, Subcommand)] +pub enum CheckSubCommand { + /// Runs all checks + All, + + /// Runs all checks but excluding network checks. + NoNetworkChecks, + + /// Only compare the current and latest available versions + CheckVersionOnly, +} + +#[derive(Debug, Args)] +pub struct MetricsCommand { + #[clap(subcommand)] + pub metrics_cmd: MetricsSubCommand, +} + +#[derive(Debug, Subcommand)] +pub enum MetricsSubCommand { + /// Arguments for metrics + MetricsArgs, +} + +// #[derive(Parser, Debug)] +#[derive(Debug, Args)] +pub struct IptablesCommand { + #[clap(subcommand)] + pub iptables: IpTablesArguments, +} + +#[derive(Debug, Subcommand)] +pub enum IpTablesArguments { + /// Configure iptables + Metrics, +} diff --git a/src/tools/kata-ctl/src/check.rs b/src/tools/kata-ctl/src/check.rs new file mode 100644 index 0000000000..28febb307c --- /dev/null +++ b/src/tools/kata-ctl/src/check.rs @@ -0,0 +1,200 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +// Contains checks that are not architecture-specific + +use anyhow::{anyhow, Result}; +use reqwest::header::{CONTENT_TYPE, USER_AGENT}; +use serde_json::Value; +use std::collections::HashMap; +use std::fs; + +const KATA_GITHUB_URL: &str = + "https://api.github.com/repos/kata-containers/kata-containers/releases/latest"; + +fn get_cpu_info(cpu_info_file: &str) -> Result { + let contents = fs::read_to_string(cpu_info_file)?; + Ok(contents) +} + +// get_single_cpu_info returns the contents of the first cpu from +// the specified cpuinfo file by parsing based on a specified delimiter +pub fn get_single_cpu_info(cpu_info_file: &str, substring: &str) -> Result { + let contents = get_cpu_info(cpu_info_file)?; + + if contents.is_empty() { + return Err(anyhow!("cpu_info string is empty"))?; + } + + let subcontents: Vec<&str> = contents.split(substring).collect(); + let result = subcontents + .first() + .ok_or("error splitting contents of cpuinfo") + .map_err(|e| anyhow!(e))? + .to_string(); + + Ok(result) +} + +// get_cpu_flags returns a string of cpu flags from cpuinfo, passed in +// as a string +pub fn get_cpu_flags(cpu_info: &str, cpu_flags_tag: &str) -> Result { + if cpu_info.is_empty() { + return Err(anyhow!("cpu_info string is empty"))?; + } + + let subcontents: Vec<&str> = cpu_info.split('\n').collect(); + for line in subcontents { + if line.starts_with(cpu_flags_tag) { + let line_data: Vec<&str> = line.split(':').collect(); + let flags = line_data + .last() + .ok_or("error splitting flags in cpuinfo") + .map_err(|e| anyhow!(e))? + .to_string(); + return Ok(flags); + } + } + + Ok("".to_string()) +} + +// get_missing_strings searches for required (strings) in data and returns +// a vector containing the missing strings +fn get_missing_strings(data: &str, required: &'static [&'static str]) -> Result> { + let mut missing: Vec = Vec::new(); + + for item in required { + if !data.split_whitespace().any(|x| x == *item) { + missing.push(item.to_string()); + } + } + + Ok(missing) +} + +pub fn check_cpu_flags( + retrieved_flags: &str, + required_flags: &'static [&'static str], +) -> Result> { + let missing_flags = get_missing_strings(retrieved_flags, required_flags)?; + + Ok(missing_flags) +} + +pub fn check_cpu_attribs( + cpu_info: &str, + required_attribs: &'static [&'static str], +) -> Result> { + let mut cpu_info_processed = cpu_info.replace('\t', ""); + cpu_info_processed = cpu_info_processed.replace('\n', " "); + + let missing_attribs = get_missing_strings(&cpu_info_processed, required_attribs)?; + Ok(missing_attribs) +} + +pub fn run_network_checks() -> Result<()> { + Ok(()) +} + +fn get_kata_version_by_url(url: &str) -> std::result::Result { + let content = reqwest::blocking::Client::new() + .get(url) + .header(CONTENT_TYPE, "application/json") + .header(USER_AGENT, "kata") + .send()? + .json::>()?; + + let version = content["tag_name"].as_str().unwrap(); + Ok(version.to_string()) +} + +fn handle_reqwest_error(e: reqwest::Error) -> anyhow::Error { + if e.is_connect() { + return anyhow!(e).context("http connection failure: connection refused"); + } + + if e.is_timeout() { + return anyhow!(e).context("http connection failure: connection timeout"); + } + + if e.is_builder() { + return anyhow!(e).context("http connection failure: url malformed"); + } + + if e.is_decode() { + return anyhow!(e).context("http connection failure: unable to decode response body"); + } + + anyhow!(e).context("unknown http connection failure: {:?}") +} + +pub fn check_version() -> Result<()> { + let version = get_kata_version_by_url(KATA_GITHUB_URL).map_err(handle_reqwest_error)?; + + println!("Version: {}", version); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use semver::Version; + + #[test] + fn test_get_cpu_info_empty_input() { + let expected = "No such file or directory (os error 2)"; + let actual = get_cpu_info("").err().unwrap().to_string(); + assert_eq!(expected, actual); + + let actual = get_single_cpu_info("", "\nprocessor") + .err() + .unwrap() + .to_string(); + assert_eq!(expected, actual); + } + + #[test] + fn test_get_cpu_flags_empty_input() { + let expected = "cpu_info string is empty"; + let actual = get_cpu_flags("", "").err().unwrap().to_string(); + assert_eq!(expected, actual); + } + + #[test] + fn check_version_by_empty_url() { + const TEST_URL: &str = "http:"; + let expected = "builder error: empty host"; + let actual = get_kata_version_by_url(TEST_URL).err().unwrap().to_string(); + assert_eq!(expected, actual); + } + + #[test] + fn check_version_by_garbage_url() { + const TEST_URL: &str = "_localhost_"; + let expected = "builder error: relative URL without a base"; + let actual = get_kata_version_by_url(TEST_URL).err().unwrap().to_string(); + assert_eq!(expected, actual); + } + + #[test] + fn check_version_by_invalid_url() { + const TEST_URL: &str = "http://localhost :80"; + let expected = "builder error: invalid domain character"; + let actual = get_kata_version_by_url(TEST_URL).err().unwrap().to_string(); + assert_eq!(expected, actual); + } + + #[test] + fn check_latest_version() { + let version = get_kata_version_by_url(KATA_GITHUB_URL).unwrap(); + + let v = Version::parse(&version).unwrap(); + assert!(!v.major.to_string().is_empty()); + assert!(!v.minor.to_string().is_empty()); + assert!(!v.patch.to_string().is_empty()); + } +} diff --git a/src/tools/kata-ctl/src/main.rs b/src/tools/kata-ctl/src/main.rs new file mode 100644 index 0000000000..30e4b5eb7a --- /dev/null +++ b/src/tools/kata-ctl/src/main.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +mod arch; +mod args; +mod check; +mod ops; + +use anyhow::Result; +use clap::Parser; +use std::process::exit; + +use args::{Commands, KataCtlCli}; + +use ops::check_ops::{ + handle_check, handle_check_volume, handle_env, handle_exec, handle_factory, handle_iptables, + handle_metrics, handle_version, +}; + +fn real_main() -> Result<()> { + let args = KataCtlCli::parse(); + + match args.command { + Commands::Check(args) => handle_check(args), + Commands::DirectVolume => handle_check_volume(), + Commands::Env => handle_env(), + Commands::Exec => handle_exec(), + Commands::Factory => handle_factory(), + Commands::Iptables(args) => handle_iptables(args), + Commands::Metrics(args) => handle_metrics(args), + Commands::Version => handle_version(), + } +} + +fn main() { + if let Err(e) = real_main() { + eprintln!("ERROR: {:#?}", e); + exit(1); + } +} diff --git a/src/tools/kata-ctl/src/ops.rs b/src/tools/kata-ctl/src/ops.rs new file mode 100644 index 0000000000..3e0f9a4e32 --- /dev/null +++ b/src/tools/kata-ctl/src/ops.rs @@ -0,0 +1,7 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +pub mod check_ops; +pub mod version; diff --git a/src/tools/kata-ctl/src/ops/check_ops.rs b/src/tools/kata-ctl/src/ops/check_ops.rs new file mode 100644 index 0000000000..b97cb6b05d --- /dev/null +++ b/src/tools/kata-ctl/src/ops/check_ops.rs @@ -0,0 +1,70 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +use crate::arch; +use crate::check; +use crate::ops::version; + +use crate::args::{CheckArgument, CheckSubCommand, IptablesCommand, MetricsCommand}; + +use anyhow::Result; + +const NAME: &str = "kata-ctl"; + +pub fn handle_check(checkcmd: CheckArgument) -> Result<()> { + let command = checkcmd.command; + + match command { + CheckSubCommand::All => { + // run architecture-specific tests + arch::check()?; + + // run code that uses network checks + check::run_network_checks()?; + } + + CheckSubCommand::NoNetworkChecks => { + // run architecture-specific tests + arch::check()?; + } + + CheckSubCommand::CheckVersionOnly => { + // retrieve latest release + check::check_version()?; + } + } + + Ok(()) +} + +pub fn handle_check_volume() -> Result<()> { + Ok(()) +} + +pub fn handle_env() -> Result<()> { + Ok(()) +} + +pub fn handle_exec() -> Result<()> { + Ok(()) +} + +pub fn handle_factory() -> Result<()> { + Ok(()) +} + +pub fn handle_iptables(_args: IptablesCommand) -> Result<()> { + Ok(()) +} + +pub fn handle_metrics(_args: MetricsCommand) -> Result<()> { + Ok(()) +} + +pub fn handle_version() -> Result<()> { + let version = version::get().unwrap(); + println!("{} version {:?} (type: rust)", NAME, version); + Ok(()) +} diff --git a/src/tools/kata-ctl/src/ops/version.rs.in b/src/tools/kata-ctl/src/ops/version.rs.in new file mode 100644 index 0000000000..052eccf168 --- /dev/null +++ b/src/tools/kata-ctl/src/ops/version.rs.in @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Intel Corporation +// +// SPDX-License-Identifier: Apache-2.0 +// + +// +// WARNING: This file is auto-generated - DO NOT EDIT! +// + +use clap::crate_version; + +const KATA_CTL_VERSION: &str = "@KATA_CTL_VERSION@"; + +pub fn get() -> Result { + if KATA_CTL_VERSION.trim().is_empty() { + Err("Unable to retrieve kata Version. Check that Kata is properly installed".to_string()) + } else { + let version = format!("{}-{}", KATA_CTL_VERSION, crate_version!()); + + Ok(version) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use semver::Version; + + #[test] + fn test_get() { + let version = get().unwrap(); + let v = Version::parse(&version).unwrap(); + + assert!(!v.major.to_string().is_empty()); + assert!(!v.minor.to_string().is_empty()); + assert!(!v.patch.to_string().is_empty()); + assert!(!v.pre.to_string().is_empty()); + } +}