hrobot/api/
vswitch.rs

1//! vSwitch structs and implementation.
2
3use std::{fmt::Display, net::IpAddr};
4
5use ipnet::IpNet;
6use serde::{Deserialize, Serialize};
7use time::Date;
8
9use crate::{error::Error, urlencode::UrlEncode, AsyncRobot};
10
11use super::{server::ServerId, wrapper::Empty, UnauthenticatedRequest};
12
13fn list_vswitches() -> UnauthenticatedRequest<Vec<VSwitchReference>> {
14    UnauthenticatedRequest::from("https://robot-ws.your-server.de/vswitch")
15}
16
17fn get_vswitch(vswitch: VSwitchId) -> UnauthenticatedRequest<InternalVSwitch> {
18    UnauthenticatedRequest::from(&format!(
19        "https://robot-ws.your-server.de/vswitch/{vswitch}"
20    ))
21}
22
23#[derive(Serialize)]
24struct UpdateVSwitch<'a> {
25    name: &'a str,
26    vlan: u16,
27}
28
29fn create_vswitch(
30    name: &str,
31    vlan_id: VlanId,
32) -> Result<UnauthenticatedRequest<VSwitchReference>, serde_html_form::ser::Error> {
33    UnauthenticatedRequest::from("https://robot-ws.your-server.de/vswitch")
34        .with_method("POST")
35        .with_body(UpdateVSwitch {
36            name,
37            vlan: vlan_id.0,
38        })
39}
40
41fn update_vswitch(
42    vswitch_id: VSwitchId,
43    name: &str,
44    vlan_id: VlanId,
45) -> Result<UnauthenticatedRequest<Empty>, serde_html_form::ser::Error> {
46    UnauthenticatedRequest::from(&format!(
47        "https://robot-ws.your-server.de/vswitch/{vswitch_id}"
48    ))
49    .with_method("POST")
50    .with_body(UpdateVSwitch {
51        name,
52        vlan: vlan_id.0,
53    })
54}
55
56fn delete_vswitch(vswitch_id: VSwitchId, date: Option<Date>) -> UnauthenticatedRequest<Empty> {
57    let date = date
58        .map(|date| date.to_string())
59        .unwrap_or("now".to_string());
60
61    UnauthenticatedRequest::from(&format!(
62        "https://robot-ws.your-server.de/vswitch/{vswitch_id}"
63    ))
64    .with_method("DELETE")
65    .with_serialized_body(format!("cancellation_date={date}"))
66}
67
68#[derive(Debug, Clone, Serialize)]
69struct ServerList<'a> {
70    server: &'a [ServerId],
71}
72
73impl<'a> UrlEncode for ServerList<'a> {
74    fn encode_into(&self, mut f: crate::urlencode::UrlEncodingBuffer<'_>) {
75        for server in self.server {
76            f.set("server[]", server);
77        }
78    }
79}
80
81fn add_servers(vswitch_id: VSwitchId, servers: &[ServerId]) -> UnauthenticatedRequest<Empty> {
82    UnauthenticatedRequest::from(&format!(
83        "https://robot-ws.your-server.de/vswitch/{vswitch_id}/server"
84    ))
85    .with_method("POST")
86    .with_serialized_body(ServerList { server: servers }.encode())
87}
88
89fn remove_servers(vswitch_id: VSwitchId, servers: &[ServerId]) -> UnauthenticatedRequest<Empty> {
90    UnauthenticatedRequest::from(&format!(
91        "https://robot-ws.your-server.de/vswitch/{vswitch_id}/server"
92    ))
93    .with_method("DELETE")
94    .with_serialized_body(ServerList { server: servers }.encode())
95}
96
97impl AsyncRobot {
98    /// List all vSwitches.
99    ///
100    /// # Example
101    /// ```rust,no_run
102    /// # #[tokio::main]
103    /// # async fn main() {
104    /// let robot = hrobot::AsyncRobot::default();
105    /// robot.list_vswitches().await.unwrap();
106    /// # }
107    /// ```
108    pub async fn list_vswitches(&self) -> Result<Vec<VSwitchReference>, Error> {
109        self.go(list_vswitches()).await
110    }
111
112    /// Get vSwitch information.
113    ///
114    /// # Example
115    /// ```rust,no_run
116    /// # use hrobot::api::vswitch::VSwitchId;
117    /// # #[tokio::main]
118    /// # async fn main() {
119    /// let robot = hrobot::AsyncRobot::default();
120    /// robot.get_vswitch(VSwitchId(123456)).await.unwrap();
121    /// # }
122    /// ```
123    pub async fn get_vswitch(&self, vswitch: VSwitchId) -> Result<VSwitch, Error> {
124        Ok(self.go(get_vswitch(vswitch)).await?.into())
125    }
126
127    /// Create a vSwitch
128    ///
129    /// # Example
130    /// ```rust,no_run
131    /// # use hrobot::api::vswitch::VlanId;
132    /// # #[tokio::main]
133    /// # async fn main() {
134    /// let robot = hrobot::AsyncRobot::default();
135    /// robot.create_vswitch("vswitch-test-1", VlanId(4078)).await.unwrap();
136    /// # }
137    /// ```
138    pub async fn create_vswitch(
139        &self,
140        name: &str,
141        vlan_id: VlanId,
142    ) -> Result<VSwitchReference, Error> {
143        self.go(create_vswitch(name, vlan_id)?).await
144    }
145
146    /// Update vSwitch.
147    ///
148    /// # Example
149    /// ```rust,no_run
150    /// # use hrobot::api::vswitch::{VSwitchId, VlanId};
151    /// # #[tokio::main]
152    /// # async fn main() {
153    /// let robot = hrobot::AsyncRobot::default();
154    /// robot.update_vswitch(
155    ///     VSwitchId(124567),
156    ///     "vswitch-test-2",
157    ///     VlanId(4079)
158    /// ).await.unwrap();
159    /// # }
160    /// ```
161    pub async fn update_vswitch(
162        &self,
163        vswitch_id: VSwitchId,
164        name: &str,
165        vlan_id: VlanId,
166    ) -> Result<(), Error> {
167        self.go(update_vswitch(vswitch_id, name, vlan_id)?)
168            .await?
169            .throw_away();
170        Ok(())
171    }
172
173    /// Cancel vSwitch.
174    ///
175    /// If cancellation date is ommitted, the cancellation is immediate.
176    ///
177    /// # Example
178    /// ```rust,no_run
179    /// # use hrobot::api::vswitch::VSwitchId;
180    /// # use hrobot::time::{Date, Month};
181    /// # #[tokio::main]
182    /// # async fn main() {
183    /// let robot = hrobot::AsyncRobot::default();
184    /// robot.cancel_vswitch(
185    ///     VSwitchId(124567),
186    ///     Some(Date::from_calendar_date(2023, Month::July, 10).unwrap())
187    /// ).await.unwrap();
188    /// # }
189    /// ```
190    pub async fn cancel_vswitch(
191        &self,
192        vswitch_id: VSwitchId,
193        cancellation_date: Option<Date>,
194    ) -> Result<(), Error> {
195        self.go(delete_vswitch(vswitch_id, cancellation_date))
196            .await?
197            .throw_away();
198        Ok(())
199    }
200
201    /// Connect dedicated servers to vSwitch.
202    ///
203    /// # Example
204    /// ```rust,no_run
205    /// # use hrobot::api::vswitch::VSwitchId;
206    /// # use hrobot::api::server::ServerId;
207    /// # #[tokio::main]
208    /// # async fn main() {
209    /// let robot = hrobot::AsyncRobot::default();
210    /// robot.connect_vswitch_servers(
211    ///     VSwitchId(124567),
212    ///     &[ServerId(1234567)],
213    /// ).await.unwrap();
214    /// # }
215    /// ```
216    pub async fn connect_vswitch_servers(
217        &self,
218        vswitch_id: VSwitchId,
219        server_ids: &[ServerId],
220    ) -> Result<(), Error> {
221        self.go(add_servers(vswitch_id, server_ids))
222            .await?
223            .throw_away();
224        Ok(())
225    }
226
227    /// Disconnect dedicated servers from vSwitch.
228    ///
229    /// # Example
230    /// ```rust,no_run
231    /// # use hrobot::api::vswitch::VSwitchId;
232    /// # use hrobot::api::server::ServerId;
233    /// # #[tokio::main]
234    /// # async fn main() {
235    /// let robot = hrobot::AsyncRobot::default();
236    /// robot.disconnect_vswitch_servers(
237    ///     VSwitchId(124567),
238    ///     &[ServerId(1234567)],
239    /// ).await.unwrap();
240    /// # }
241    /// ```
242    pub async fn disconnect_vswitch_servers(
243        &self,
244        vswitch_id: VSwitchId,
245        server_ids: &[ServerId],
246    ) -> Result<(), Error> {
247        self.go(remove_servers(vswitch_id, server_ids))
248            .await?
249            .throw_away();
250        Ok(())
251    }
252}
253
254/// VLAN ID.
255///
256/// Simple wrapper around a u16, to avoid confusion with vSwitch ID, for example.
257///
258/// VLAN IDs must be in the range 4000..=4091.
259///
260/// Multiple vSwitches can have the same VLAN ID.
261#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
262pub struct VlanId(pub u16);
263
264impl From<u16> for VlanId {
265    fn from(value: u16) -> Self {
266        VlanId(value)
267    }
268}
269
270impl From<VlanId> for u16 {
271    fn from(value: VlanId) -> Self {
272        value.0
273    }
274}
275
276impl Display for VlanId {
277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
278        self.0.fmt(f)
279    }
280}
281
282impl PartialEq<u16> for VlanId {
283    fn eq(&self, other: &u16) -> bool {
284        self.0.eq(other)
285    }
286}
287
288/// Uniquely identifies a vSwitch.
289///
290/// Simple wrapper around a u32, to avoid confusion with other simple integer-based IDs
291/// such as [`VlanId`] and to make it intuitive what kind
292/// of argument you need to give to functions.
293///
294/// Using a plain integer means it isn't clear what the argument is, is it a counter of
295/// my vSwitches, where the argument is in range `0..N` where `N` is the number of
296/// vswitches in my account, or is it a limiter, like get first `N` vswitches, for example.
297#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
298pub struct VSwitchId(pub u32);
299
300impl From<u32> for VSwitchId {
301    fn from(value: u32) -> Self {
302        VSwitchId(value)
303    }
304}
305
306impl From<VSwitchId> for u32 {
307    fn from(value: VSwitchId) -> Self {
308        value.0
309    }
310}
311
312impl Display for VSwitchId {
313    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
314        self.0.fmt(f)
315    }
316}
317
318impl PartialEq<u32> for VSwitchId {
319    fn eq(&self, other: &u32) -> bool {
320        self.0.eq(other)
321    }
322}
323
324/// Simplified view of a VSwitch.
325///
326/// This is returned when [listing](AsyncRobot::list_vswitches()) vSwitches, and only contains
327/// the basic vSwitch configuration options. For information on which servers, subnets and
328/// cloud networks are connected to the vSwitch see [`AsyncRobot::get_vswitch`]
329#[derive(Debug, Clone, Serialize, Deserialize)]
330pub struct VSwitchReference {
331    /// Unique vSwitch ID.
332    pub id: VSwitchId,
333
334    /// Name of the vSwitch
335    pub name: String,
336
337    /// VLAN ID for the vSwitch.
338    ///
339    /// VLAN IDs must be in the range 4000..=4091.
340    pub vlan: VlanId,
341
342    /// Indicates if the vSwitch has been cancelled or not.
343    pub cancelled: bool,
344}
345
346#[derive(Debug, Clone, Serialize, Deserialize)]
347struct InternalVSwitch {
348    pub id: VSwitchId,
349    pub name: String,
350    pub vlan: VlanId,
351    pub cancelled: bool,
352    pub server: Vec<VSwitchServer>,
353    pub subnet: Vec<InternalSubnet>,
354    pub cloud_network: Vec<InternalCloudNetwork>,
355}
356
357impl From<InternalVSwitch> for VSwitch {
358    fn from(value: InternalVSwitch) -> Self {
359        VSwitch {
360            id: value.id,
361            name: value.name,
362            vlan: value.vlan,
363            cancelled: value.cancelled,
364            servers: value.server,
365            subnets: value.subnet.into_iter().map(IpNet::from).collect(),
366            cloud_networks: value
367                .cloud_network
368                .into_iter()
369                .map(CloudNetwork::from)
370                .collect(),
371        }
372    }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
376struct InternalSubnet {
377    pub ip: IpAddr,
378    pub mask: u8,
379}
380
381impl From<InternalSubnet> for IpNet {
382    fn from(value: InternalSubnet) -> Self {
383        IpNet::new(value.ip, value.mask).unwrap()
384    }
385}
386
387/// Describes a complete vSwitch configuration.
388#[derive(Debug, Clone)]
389pub struct VSwitch {
390    /// Unique vSwitch ID.
391    pub id: VSwitchId,
392
393    /// Name for this vSwitch.
394    pub name: String,
395
396    /// VLAN ID associated with traffic over this vSwitch.
397    pub vlan: VlanId,
398
399    /// Indicates if the vSwitch has been cancelled.
400    pub cancelled: bool,
401
402    /// List of servers connected to this vSwitch.
403    pub servers: Vec<VSwitchServer>,
404
405    /// List of subnets associated with this vSwitch.
406    pub subnets: Vec<IpNet>,
407
408    /// List of Cloud Networks connected to this vSwitch.
409    pub cloud_networks: Vec<CloudNetwork>,
410}
411
412/// Indicates the connection status of a server to a vSwitch.
413///
414/// Connecting or disconnecting a server to/from a vSwitch requires some
415/// processing time, and the server won't be immediately available on the vSwitch
416/// network.
417#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
418pub enum ConnectionStatus {
419    /// Server is connected and ready.
420    #[serde(rename = "ready")]
421    Ready,
422
423    /// Server is currently in the process of connecting or disconnecting from the vSwitch.
424    #[serde(rename = "in process", alias = "processing")]
425    InProcess,
426
427    /// Server connect/disconnect failed.
428    #[serde(rename = "failed")]
429    Failed,
430}
431
432/// Connection status of a server to a vSwitch.
433#[derive(Debug, Clone, Serialize, Deserialize)]
434pub struct VSwitchServer {
435    /// Server's unique ID.
436    #[serde(rename = "server_number")]
437    pub id: ServerId,
438
439    /// Status of the server's connection to the vSwitch.
440    pub status: ConnectionStatus,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
444struct InternalCloudNetwork {
445    pub id: CloudNetworkId,
446    pub ip: IpAddr,
447    pub mask: u8,
448}
449
450impl From<InternalCloudNetwork> for CloudNetwork {
451    fn from(value: InternalCloudNetwork) -> Self {
452        CloudNetwork {
453            id: value.id,
454            network: IpNet::new(value.ip, value.mask).unwrap(),
455        }
456    }
457}
458
459/// Identifies a Cloud Network connected to a vSwitch.
460#[derive(Debug, Clone, PartialEq, Eq)]
461pub struct CloudNetwork {
462    /// Unique ID for the Cloud Network the vSwitch is connected to.
463    pub id: CloudNetworkId,
464
465    /// Subnet of the Cloud Network the vSwitch inhabits.
466    pub network: IpNet,
467}
468
469/// Cloud Network unique ID.
470///
471/// Simple wrapper around a u32, to avoid confusion with for example [`VSwitchId`]
472#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
473pub struct CloudNetworkId(pub u32);
474
475impl From<u32> for CloudNetworkId {
476    fn from(value: u32) -> Self {
477        CloudNetworkId(value)
478    }
479}
480
481impl From<CloudNetworkId> for u32 {
482    fn from(value: CloudNetworkId) -> Self {
483        value.0
484    }
485}
486
487impl Display for CloudNetworkId {
488    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
489        self.0.fmt(f)
490    }
491}
492
493impl PartialEq<u32> for CloudNetworkId {
494    fn eq(&self, other: &u32) -> bool {
495        self.0.eq(other)
496    }
497}
498
499#[cfg(test)]
500mod tests {
501    use std::{
502        net::{IpAddr, Ipv4Addr},
503        str::FromStr,
504    };
505
506    use ipnet::{IpNet, Ipv4Net};
507
508    use crate::api::vswitch::{
509        CloudNetwork, CloudNetworkId, InternalCloudNetwork, InternalSubnet, VSwitchId, VlanId,
510    };
511
512    use super::InternalVSwitch;
513
514    #[test]
515    fn deserialize_vswitch() {
516        let json = r#"
517        {
518            "id": 50301,
519            "name": "hrobot-test-vswitch-AOLwCPri-re",
520            "vlan":4001,
521            "cancelled":false,
522            "server":[
523                {
524                    "server_number": 2321379,
525                    "server_ip": "138.201.21.47",
526                    "server_ipv6_net": "2a01:4f8:171:2c2c::",
527                    "status": "processing"
528                }
529            ],
530            "subnet": [],
531            "cloud_network": []
532        }"#;
533
534        let _ = serde_json::from_str::<InternalVSwitch>(json).unwrap();
535    }
536
537    #[test]
538    fn vlan_construction() {
539        assert_eq!(VlanId::from(4001u16), 4001);
540
541        assert_eq!(VlanId(4001).to_string(), "4001");
542    }
543
544    #[test]
545    fn vswitch_id_construction() {
546        assert_eq!(VSwitchId::from(10101u32), 10101u32);
547        assert_eq!(
548            VSwitchId::from(10101u32),
549            u32::from(VSwitchId::from(10101u32)),
550        );
551    }
552
553    #[test]
554    fn internal_subnet_conversion() {
555        assert_eq!(
556            IpNet::from(InternalSubnet {
557                ip: IpAddr::from_str("127.0.0.0").unwrap(),
558                mask: 24
559            }),
560            IpNet::V4(Ipv4Net::new(Ipv4Addr::new(127, 0, 0, 0), 24).unwrap())
561        );
562    }
563
564    #[test]
565    fn cloud_network_construction() {
566        assert_eq!(
567            CloudNetwork::from(InternalCloudNetwork {
568                id: CloudNetworkId::from(10),
569                ip: Ipv4Addr::LOCALHOST.into(),
570                mask: 8
571            }),
572            CloudNetwork {
573                id: CloudNetworkId(10),
574                network: IpNet::new(Ipv4Addr::LOCALHOST.into(), 8).unwrap()
575            }
576        );
577
578        assert_eq!(u32::from(CloudNetworkId(10)), 10);
579        assert_eq!(CloudNetworkId(10), 10);
580    }
581}