diff --git a/Cargo.toml b/Cargo.toml index 3e1dab6312..f069b1395e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,12 +15,13 @@ edition = "2018" [features] -default = ["compute", "image", "network", "native-tls", "object-storage"] +default = ["compute", "image", "network", "native-tls", "object-storage", "orchestration"] compute = [] image = [] network = [] native-tls = ["reqwest/default-tls", "osauth/native-tls"] object-storage = [] +orchestration = [] rustls = ["reqwest/rustls-tls", "osauth/rustls"] [dependencies] diff --git a/examples/create-stack.rs b/examples/create-stack.rs new file mode 100644 index 0000000000..0f8073c978 --- /dev/null +++ b/examples/create-stack.rs @@ -0,0 +1,58 @@ +// Copyright 2018 Dmitry Tantsur +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +extern crate env_logger; +extern crate openstack; +extern crate waiter; + +use std::env; +use waiter::{Waiter, WaiterCurrentState}; + +#[cfg(feature = "orchestration")] +fn main() { + env_logger::init(); + + let os = openstack::Cloud::from_env() + .expect("Failed to create an identity provider from the environment"); + + let stackf = env::args().nth(1).expect("Provide a stack file"); + let envf = env::args().nth(2).expect("Provide an env file"); + + let waiter = os + .new_stack(stackf, envf) + .create() + .expect("Cannot create a server"); + { + let current = waiter.waiter_current_state(); + println!( + "ID = {}, Name = {}, Status = {:?}", + current.id(), + current.name(), + current.status(), + ); + } + + let server = waiter.wait().expect("Stack did not reach COMPLETE"); + println!( + "ID = {}, Name = {}, Status = {:?}", + server.id(), + server.name(), + server.status(), + ); +} + +#[cfg(not(feature = "orchestration"))] +fn main() { + panic!("This example cannot run with 'orchestration' feature disabled"); +} diff --git a/src/cloud.rs b/src/cloud.rs index deadc55626..0ddacf3ebd 100644 --- a/src/cloud.rs +++ b/src/cloud.rs @@ -30,6 +30,8 @@ use super::compute::{ Flavor, FlavorQuery, FlavorSummary, KeyPair, KeyPairQuery, NewKeyPair, NewServer, Server, ServerQuery, ServerSummary, }; +#[cfg(feature = "orchestration")] +use super::orchestration::NewStack; #[cfg(feature = "image")] use super::image::{Image, ImageQuery}; #[cfg(feature = "network")] @@ -669,6 +671,20 @@ impl Cloud { NewServer::new(self.session.clone(), name.into(), flavor.into()) } + /// Prepare a new stack for creation. + /// + /// This call returns a `NewStack` object, which is a builder to populate + /// stack fields. + #[cfg(feature = "orchestration")] + pub fn new_stack(&self, stackf: S1, envf: S2) -> NewStack + where + S1: Into, + S1: Into, + { + NewStack::new(self.session.clone(), stackf.into(), envf.into()) + } + + /// Prepare a new subnet for creation. /// /// This call returns a `NewSubnet` object, which is a builder to populate diff --git a/src/lib.rs b/src/lib.rs index 59a19cafa3..9f7b127641 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -656,6 +656,8 @@ mod cloud; pub mod common; #[cfg(feature = "compute")] pub mod compute; +#[cfg(feature = "orchestration")] +pub mod orchestration; #[cfg(feature = "image")] pub mod image; #[cfg(feature = "network")] diff --git a/src/orchestration/mod.rs b/src/orchestration/mod.rs new file mode 100644 index 0000000000..002ceb7949 --- /dev/null +++ b/src/orchestration/mod.rs @@ -0,0 +1,190 @@ +// Copyright 2017 Dmitry Tantsur +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Stack management via Orchestration API. +use std::rc::Rc; +use std::time::Duration; + +use chrono::{DateTime, FixedOffset}; +use waiter::{Waiter, WaiterCurrentState}; +use super::{Error, ErrorKind, Result}; + +mod protocol; +//mod api; + +use super::common::{ + DeletionWaiter, + Refresh, +}; +use super::session::Session; + +/// A request to create a stack. +#[derive(Debug)] +pub struct NewStack { +} + +/// Structure representing a single stack. +#[derive(Clone, Debug)] +pub struct Stack { + session: Rc, + inner: protocol::Stack, +} + +impl NewStack { + /// Start creating a server. + pub(crate) fn new(session: Rc, name: String) -> NewStack { + NewServer { + // session, + } + } + + /// Request creation of the server. + pub fn create(self) -> Result { + let request = protocol::StackCreate { + name: self.name, + }; + + let stack_ref = api::create_stack(&self.session, request)?; + Ok(StackCreationWaiter { + stack: Stack::load(self.session, stack_ref.id)?, + }) + } +} + +/// Waiter for server to be created. +#[derive(Debug)] +pub struct StackCreationWaiter { + stack: Stack, +} + +/// Waiter for stack status to change. +#[derive(Debug)] +pub struct StackStatusWaiter<'stack> { + stack: &'stack mut Stack, + target: protocol::StackStatus, +} + +impl Refresh for Stack { + /// Refresh the stack. + fn refresh(&mut self) -> Result<()> { + //self.inner = api::get_stack_by_id(&self.session, &self.inner.id)?; + Ok(()) + } +} + + +impl Stack { + /// Create a new Stack object. + pub(crate) fn new(session: Rc, inner: protocol::Stack) -> Result { + Ok(Stack { + session, + inner, + }) + } + + /// Load a Stack object. + pub(crate) fn load>(session: Rc, id: Id) -> Result { + let inner = api::get_stack(&session, id)?; + Stack::new(session, inner) + } + + transparent_property! { + #[doc = "Creation date and time."] + created_at: DateTime + } + + transparent_property! { + #[doc = "Stack description."] + description: ref Option + } + + transparent_property! { + #[doc = "Stack unique ID."] + id: ref String + } + + transparent_property! { + #[doc = "Stack name."] + name: ref String + } + + transparent_property! { + #[doc = "Stack status."] + status: protocol::StackStatus + } + + /// Delete the stack. + pub fn delete(self) -> Result> { + api::delete_stack(&self.session, &self.inner.id)?; + Ok(DeletionWaiter::new( + self, + Duration::new(120, 0), + Duration::new(1, 0), + )) + } +} + +impl<'server> Waiter<(), Error> for StackStatusWaiter<'server> { + fn default_wait_timeout(&self) -> Option { + // TODO(dtantsur): vary depending on target? + Some(Duration::new(600, 0)) + } + + fn default_delay(&self) -> Duration { + Duration::new(1, 0) + } + + fn timeout_error(&self) -> Error { + Error::new( + ErrorKind::OperationTimedOut, + format!( + "Timeout waiting for stack {} to reach state {}", + self.stack.id(), + self.target + ), + ) + } + + fn poll(&mut self) -> Result> { + self.stack.refresh()?; + if self.stack.status() == self.target { + debug!("Stack {} reached state {}", self.stack.id(), self.target); + Ok(Some(())) + } else if self.stack.status() == protocol::StackStatus::Failed { + debug!( + "Failed to move stack {} to {} - status is ERROR", + self.stack.id(), + self.target + ); + Err(Error::new( + ErrorKind::OperationFailed, + format!("Stack {} got into ERROR state", self.stack.id()), + )) + } else { + trace!( + "Still waiting for stack {} to get to state {}, current is {}", + self.stack.id(), + self.target, + self.stack.status() + ); + Ok(None) + } + } +} + +impl<'server> WaiterCurrentState for StackStatusWaiter<'server> { + fn waiter_current_state(&self) -> &Stack { + &self.stack + } +} diff --git a/src/orchestration/protocol.rs b/src/orchestration/protocol.rs new file mode 100644 index 0000000000..f05ee12f06 --- /dev/null +++ b/src/orchestration/protocol.rs @@ -0,0 +1,55 @@ +// Copyright 2017 Dmitry Tantsur +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! JSON structures and protocol bits for the Orchestration API. + +#![allow(non_snake_case)] +#![allow(missing_docs)] + +use chrono::{DateTime, FixedOffset}; +use osproto::common::empty_as_default; +use serde::{Deserialize, Serialize}; + + +protocol_enum! { + #[doc = "Possible server statuses."] + enum StackStatus { + Complete = "CREATE_COMPLETE", + Failed = "CREATE_COMPLETE", + InProgress = "CREATE_IN_PROGRESS", + Unknown = "UNKNOWN" + } +} + +#[derive(Clone, Debug, Deserialize)] +pub struct Stack { + #[serde(rename = "created")] + pub created_at: DateTime, + #[serde(deserialize_with = "empty_as_default", default)] + pub description: Option, + pub id: String, + pub name: String, + pub status: StackStatus, +} + +#[derive(Clone, Debug, Serialize)] +pub struct StackCreate { + pub name: String, +} + +impl Default for StackStatus { + fn default() -> StackStatus { + StackStatus::Unknown + } +}