libstorage/
brocade.rs

1use crate::error::{MetricsResult, StorageError};
2use crate::ir::{TsPoint, TsValue};
3use crate::IntoPoint;
4use chrono::offset::Utc;
5use chrono::DateTime;
6use log::{error, trace};
7use reqwest::header::{HeaderMap, HeaderValue, ACCEPT};
8use serde::de::DeserializeOwned;
9/**
10* Copyright 2019 Comcast Cable Communications Management, LLC
11*
12* Licensed under the Apache License, Version 2.0 (the "License");
13* you may not use this file except in compliance with the License.
14* You may obtain a copy of the License at
15*
16* http://www.apache.org/licenses/LICENSE-2.0
17*
18* Unless required by applicable law or agreed to in writing, software
19* distributed under the License is distributed on an "AS IS" BASIS,
20* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21* See the License for the specific language governing permissions and
22* limitations under the License.
23*
24* SPDX-License-Identifier: Apache-2.0
25*/
26use std::fmt::Debug;
27
28#[derive(Clone, Deserialize, Debug)]
29pub struct BrocadeConfig {
30    /// The brocade endpoint to use
31    pub endpoint: String,
32    pub user: String,
33    /// This gets replaced with the token at runtime
34    pub password: String,
35    /// Optional certificate file to use against the server
36    /// der encoded
37    pub certificate: Option<String>,
38    /// Optional root certificate file to use against the server
39    /// der encoded
40    pub root_certificate: Option<String>,
41    /// The region this cluster is located in
42    pub region: String,
43}
44
45pub struct Brocade {
46    client: reqwest::Client,
47    config: BrocadeConfig,
48    token: String,
49}
50
51impl Brocade {
52    /// Initialize and connect to a Brocade switch.
53    pub fn new(client: &reqwest::Client, config: BrocadeConfig) -> MetricsResult<Self> {
54        let token = login(client, &config)?;
55        Ok(Brocade {
56            client: client.clone(),
57            config,
58            token,
59        })
60    }
61}
62
63impl Drop for Brocade {
64    fn drop(&mut self) {
65        if let Err(e) = self.logout() {
66            error!("logout failed: {}", e);
67        }
68    }
69}
70
71#[test]
72fn parse_resource_groups() {
73    use std::fs::File;
74    use std::io::Read;
75
76    let mut f = File::open("tests/brocade/resource_groups.json").unwrap();
77    let mut buff = String::new();
78    f.read_to_string(&mut buff).unwrap();
79
80    let i: ResourceGroups = serde_json::from_str(&buff).unwrap();
81    println!("result: {:#?}", i);
82}
83#[serde(rename_all = "camelCase")]
84#[derive(Deserialize, Debug)]
85pub struct ResourceGroups {
86    pub resource_groups: Vec<ResourceGroup>,
87}
88
89#[derive(Deserialize, Debug)]
90pub struct ResourceGroup {
91    pub key: String,
92    pub name: String,
93    #[serde(rename = "type")]
94    pub resource_type: String,
95}
96
97#[serde(rename_all = "camelCase")]
98#[derive(Deserialize, Debug)]
99pub struct BufferCredit {
100    pub bb_credit: u64,
101    #[serde(rename = "peerBBCredit")]
102    pub peer_bb_credit: u64,
103    pub round_trip_time: u64,
104}
105
106#[test]
107fn parse_fc_fabrics() {
108    use std::fs::File;
109    use std::io::Read;
110
111    sleep_the_collections();
112
113    let mut f = File::open("tests/brocade/fcfabrics.json").unwrap();
114    let mut buff = String::new();
115    f.read_to_string(&mut buff).unwrap();
116
117    let i: FcFabrics = serde_json::from_str(&buff).unwrap();
118    println!("result: {:#?}", i);
119}
120
121#[serde(rename_all = "camelCase")]
122#[derive(Deserialize, Debug)]
123pub struct FcFabrics {
124    pub fc_fabrics: Vec<FcFabric>,
125    pub start_index: Option<i32>,
126    pub items_per_page: Option<i32>,
127    pub total_results: Option<u64>,
128}
129
130#[serde(rename_all = "camelCase")]
131#[derive(Deserialize, Debug, IntoPoint)]
132pub struct FcFabric {
133    pub key: String,
134    pub seed_switch_wwn: String,
135    pub name: String,
136    pub secure: bool,
137    pub ad_environment: bool,
138    pub contact: Option<String>,
139    pub location: Option<String>,
140    pub description: Option<String>,
141    pub principal_switch_wwn: String,
142    pub fabric_name: String,
143    pub virtual_fabric_id: i32,
144    pub seed_switch_ip_address: String,
145}
146
147#[serde(rename_all = "camelCase")]
148#[derive(Deserialize, Debug)]
149pub struct FcPorts {
150    pub fc_ports: Vec<FcPort>,
151    pub start_index: Option<i32>,
152    pub items_per_page: Option<i32>,
153    pub total_results: Option<u64>,
154}
155
156#[test]
157fn parse_fc_ports() {
158    use std::fs::File;
159    use std::io::Read;
160
161    let mut f = File::open("tests/brocade/fcports.json").unwrap();
162    let mut buff = String::new();
163    f.read_to_string(&mut buff).unwrap();
164
165    let i: FcPorts = serde_json::from_str(&buff).unwrap();
166    println!("result: {:#?}", i);
167}
168
169#[serde(rename_all = "camelCase")]
170#[derive(Deserialize, Debug, IntoPoint)]
171pub struct FcPort {
172    key: String,
173    wwn: String,
174    name: String,
175    slot_number: u64,
176    port_number: u64,
177    user_port_number: u64,
178    port_id: String,
179    port_index: u64,
180    area_id: u64,
181    #[serde(rename = "type")]
182    port_type: String,
183    status: String,
184    status_message: String,
185    locked_port_type: String,
186    speed: String,
187    speeds_supported: String,
188    max_port_speed: u16,
189    desired_credits: u64,
190    buffer_allocated: u64,
191    estimated_distance: u64,
192    actual_distance: u64,
193    long_distance_setting: u64,
194    remote_node_wwn: String,
195    remote_port_wwn: String,
196    licensed: bool,
197    swapped: bool,
198    trunked: bool,
199    trunk_master: bool,
200    persistently_disabled: bool,
201    ficon_supported: bool,
202    blocked: bool,
203    prohibit_port_numbers: Option<String>,
204    prohibit_port_count: u64,
205    npiv_capable: bool,
206    npiv_enabled: bool,
207    fc_fast_write_enabled: bool,
208    isl_rrdy_enabled: bool,
209    rate_limit_capable: bool,
210    rate_limited: bool,
211    qos_capable: bool,
212    qos_enabled: bool,
213    fcr_fabric_id: u64,
214    state: String,
215    occupied: bool,
216    master_port_number: i64,
217}
218
219#[serde(rename_all = "camelCase")]
220#[derive(Deserialize, Debug)]
221pub struct Fec {
222    pub corrected_blocks: u64,
223    pub uncorrected_blocks: u64,
224}
225
226#[test]
227fn parse_fc_switches() {
228    use std::fs::File;
229    use std::io::Read;
230
231    let mut f = File::open("tests/brocade/fcswitches.json").unwrap();
232    let mut buff = String::new();
233    f.read_to_string(&mut buff).unwrap();
234
235    let i: FcSwitches = serde_json::from_str(&buff).unwrap();
236    println!("result: {:#?}", i);
237}
238
239#[serde(rename_all = "camelCase")]
240#[derive(Deserialize, Debug)]
241pub struct FcSwitches {
242    pub fc_switches: Vec<FcSwitch>,
243    pub start_index: Option<i32>,
244    pub items_per_page: Option<i32>,
245    pub total_results: Option<u64>,
246}
247
248#[serde(rename_all = "camelCase")]
249#[derive(Deserialize, Debug, IntoPoint)]
250pub struct FcSwitch {
251    pub key: String,
252    #[serde(rename = "type")]
253    pub fc_type: u64,
254    pub name: String,
255    pub wwn: String,
256    pub virtual_fabric_id: i64,
257    pub domain_id: u64,
258    pub base_switch: bool,
259    pub role: String,
260    pub fcs_role: String,
261    pub ad_capable: bool,
262    pub operational_status: String,
263    pub state: String,
264    pub status_reason: Option<String>,
265    pub lf_enabled: bool,
266    pub default_logical_switch: bool,
267    pub fms_mode: bool,
268    pub dynamic_load_sharing_capable: bool,
269    pub port_based_routing_present: bool,
270    pub in_order_delivery_capable: bool,
271    pub persistent_did_enabled: bool,
272    pub auto_snmp_enabled: bool,
273}
274
275pub enum FabricTimeSeries {
276    MemoryUtilPercentage,
277    CpuUtilPercentage,
278    Temperature,
279    FanSpeed,
280    ResponseTime,
281    SystemUpTime,
282    PortsNotInUse,
283    PingPktLossPercentage,
284}
285
286impl ToString for FabricTimeSeries {
287    fn to_string(&self) -> String {
288        match *self {
289            FabricTimeSeries::MemoryUtilPercentage => "timeseriesmemoryutilpercentage".into(),
290            FabricTimeSeries::CpuUtilPercentage => "timeseriescpuutilpercentage".into(),
291            FabricTimeSeries::Temperature => "timeseriestemperature".into(),
292            FabricTimeSeries::FanSpeed => "timeseriesfanspeed".into(),
293            FabricTimeSeries::ResponseTime => "timeseriesresponsetime".into(),
294            FabricTimeSeries::SystemUpTime => "timeseriessystemuptime".into(),
295            FabricTimeSeries::PortsNotInUse => "timeseriesportsnotinuse".into(),
296            FabricTimeSeries::PingPktLossPercentage => "timeseriespingpktlosspercentage".into(),
297        }
298    }
299}
300
301pub enum FcIpTimeSeries {
302    CompressionRatio,
303    Latency,
304    DroppedPackets,
305    LinkRetransmits,
306    TimeoutRetransmits,
307    FastRetransmits,
308    DupAckRecvd,
309    WindowSizeRtt,
310    TcpOooSegments,
311    SlowStartStatusErrors,
312    RealtimeCompressionRatio,
313}
314
315impl ToString for FcIpTimeSeries {
316    fn to_string(&self) -> String {
317        match *self {
318            FcIpTimeSeries::CompressionRatio => "timeseriescompressionratio".into(),
319            FcIpTimeSeries::Latency => "timeserieslatency".into(),
320            FcIpTimeSeries::DroppedPackets => "timeseriesdroppedpackets".into(),
321            FcIpTimeSeries::LinkRetransmits => "timeserieslinkretransmits".into(),
322            FcIpTimeSeries::TimeoutRetransmits => "timeseriestimeoutretransmits".into(),
323            FcIpTimeSeries::FastRetransmits => "timeseriesfastretransmits".into(),
324            FcIpTimeSeries::DupAckRecvd => "timeseriesdupackrecvd".into(),
325            FcIpTimeSeries::WindowSizeRtt => "timeserieswindowsizertt".into(),
326            FcIpTimeSeries::TcpOooSegments => "timeseriestcpooosegments".into(),
327            FcIpTimeSeries::SlowStartStatusErrors => "timeseriesslowstartstatuserrors".into(),
328            FcIpTimeSeries::RealtimeCompressionRatio => "timeseriesrealtimecompressionratio".into(),
329        }
330    }
331}
332pub enum FcTimeSeries {
333    UtilPercentage,
334    Traffic,
335    CrcErrors,
336    LinkResets,
337    SignalLosses,
338    SyncLosses,
339    LinkFailures,
340    SequenceErrors,
341    InvalidTransmissions,
342    C3Discards,
343    C3DiscardsTxTo,
344    C3DiscardsRxTo,
345    C3DiscardsUnreachable,
346    C3DiscardsOther,
347    EncodeErrorOut,
348    SfpPower,
349    SfpVoltage,
350    SfpCurrent,
351    SfpTemperature,
352    InvalidOrderedSets,
353    BbCreditZero,
354    TruncatedFrames,
355}
356
357impl ToString for FcTimeSeries {
358    fn to_string(&self) -> String {
359        match *self {
360            FcTimeSeries::UtilPercentage => "timeseriesutilpercentage".into(),
361            FcTimeSeries::Traffic => "timeseriestraffic".into(),
362            FcTimeSeries::CrcErrors => "timeseriescrcerrors".into(),
363            FcTimeSeries::LinkResets => "timeserieslinkresets".into(),
364            FcTimeSeries::SignalLosses => "timeseriessignallosses".into(),
365            FcTimeSeries::SyncLosses => "timeseriessynclosses".into(),
366            FcTimeSeries::LinkFailures => "timeserieslinkfailures".into(),
367            FcTimeSeries::SequenceErrors => "timeseriessequenceerrors".into(),
368            FcTimeSeries::InvalidTransmissions => "timeseriesinvalidtransmissions".into(),
369            FcTimeSeries::C3Discards => "timeseriesc3discards".into(),
370            FcTimeSeries::C3DiscardsTxTo => "timeseriesc3discardstxto".into(),
371            FcTimeSeries::C3DiscardsRxTo => "timeseriesc3discardsrxto".into(),
372            FcTimeSeries::C3DiscardsUnreachable => "timeseriesc3discardsunreachable".into(),
373            FcTimeSeries::C3DiscardsOther => "timeseriesc3discardsother".into(),
374            FcTimeSeries::EncodeErrorOut => "timeseriesencodeerrorout".into(),
375            FcTimeSeries::SfpPower => "timeseriessfppower".into(),
376            FcTimeSeries::SfpVoltage => "timeseriessfpvoltage".into(),
377            FcTimeSeries::SfpCurrent => "timeseriessfpcurrent".into(),
378            FcTimeSeries::SfpTemperature => "timeseriessfptemperature".into(),
379            FcTimeSeries::InvalidOrderedSets => "timeseriesinvalidorderedsets".into(),
380            FcTimeSeries::BbCreditZero => "timeseriesbbcreditzero".into(),
381            FcTimeSeries::TruncatedFrames => "timeseriestruncatedframes".into(),
382        }
383    }
384}
385
386pub enum FrameTimeSeries {
387    TxFrameCount,
388    RxFrameCount,
389    TxFrameRate,
390    RxFrameRate,
391    TxWordCount,
392    RxWordCount,
393    TxThroughput,
394    RxThroughput,
395    AvgTxFrameSize,
396    AvgRxFrameSize,
397    GeneratorTxFrameCount,
398    GeneratorRxFrameCount,
399    MirroredFramesCount,
400    MirroredTxFrames,
401    MirroredRxFrames,
402}
403
404impl ToString for FrameTimeSeries {
405    fn to_string(&self) -> String {
406        match *self {
407            FrameTimeSeries::TxFrameCount => "timeseriestxframecount".into(),
408            FrameTimeSeries::RxFrameCount => "timeseriesrxframecount".into(),
409            FrameTimeSeries::TxFrameRate => "timeseriestxframerate".into(),
410            FrameTimeSeries::RxFrameRate => "timeseriesrxframerate".into(),
411            FrameTimeSeries::TxWordCount => "timeseriestxwordcount".into(),
412            FrameTimeSeries::RxWordCount => "timeseriesrxwordcount".into(),
413            FrameTimeSeries::TxThroughput => "timeseriestxthroughput".into(),
414            FrameTimeSeries::RxThroughput => "timeseriesrxthroughput".into(),
415            FrameTimeSeries::AvgTxFrameSize => "timeseriesavgtxframesize".into(),
416            FrameTimeSeries::AvgRxFrameSize => "timeseriesavgrxframesize".into(),
417            FrameTimeSeries::GeneratorTxFrameCount => "timeseriesgeneratortxframecount".into(),
418            FrameTimeSeries::GeneratorRxFrameCount => "timeseriesgeneratorrxframecount".into(),
419            FrameTimeSeries::MirroredFramesCount => "timeseriesmirroredframescount".into(),
420            FrameTimeSeries::MirroredTxFrames => "timeseriesmirroredtxframes".into(),
421            FrameTimeSeries::MirroredRxFrames => "timeseriesmirroredrxframes".into(),
422        }
423    }
424}
425
426pub struct ReadDiagnostic {
427    pub switch_name: String,
428    pub switch_wwn: String,
429    pub number_of_ports: u64,
430    pub stats_type: RdpStatsType,
431    pub port_wwn: String,
432    pub port_type: String,
433    pub node_wwn: String,
434    pub tx_power: String,
435    pub rx_power: String,
436    pub temperature: String,
437    pub sfp_type: String,
438    pub laser_type: String,
439    pub voltage: String,
440    pub current: String,
441    pub connecter_type: String,
442    pub supported_speeds: String,
443    pub link_failure: u64,
444    pub loss_of_sync: u64,
445    pub loss_of_signal: u64,
446    pub protocol_error: u64,
447    pub invalid_word: u64,
448    pub invalid_crc: u64,
449    pub fec: Fec,
450    pub buffer_credit: BufferCredit,
451}
452
453pub enum RdpStatsType {
454    Historical,
455    Realtime,
456}
457
458pub enum ScsiTimeSeries {
459    ReadFrameCount,
460    WriteFrameCount,
461    ReadFrameRate,
462    WriteFrameRate,
463    ReadData,
464    WriteData,
465    ReadDataRate,
466    WriteDataRate,
467}
468
469impl ToString for ScsiTimeSeries {
470    fn to_string(&self) -> String {
471        match *self {
472            ScsiTimeSeries::ReadFrameCount => "timeseriesscsireadframecount".into(),
473            ScsiTimeSeries::WriteFrameCount => "timeseriesscsiwriteframecount".into(),
474            ScsiTimeSeries::ReadFrameRate => "timeseriesscsireadframerate".into(),
475            ScsiTimeSeries::WriteFrameRate => "timeseriesscsiwriteframerate".into(),
476            ScsiTimeSeries::ReadData => "timeseriesscsireaddata".into(),
477            ScsiTimeSeries::WriteData => "timeseriesscsiwritedata".into(),
478            ScsiTimeSeries::ReadDataRate => "timeseriesscsireaddatarate".into(),
479            ScsiTimeSeries::WriteDataRate => "timeseriesscsiwritedatarate".into(),
480        }
481    }
482}
483
484pub enum TimeSeries {
485    Fc(FcTimeSeries),
486    FcIp(FcIpTimeSeries),
487}
488
489// Connect to the server and request a new api token
490pub fn login(client: &reqwest::Client, config: &BrocadeConfig) -> MetricsResult<String> {
491    let mut headers = HeaderMap::new();
492    headers.insert(
493        ACCEPT,
494        HeaderValue::from_str("application/vnd.brocade.networkadvisor+json;version=v1")?,
495    );
496    headers.insert("WSUsername", HeaderValue::from_str(&config.user)?);
497    headers.insert("WSPassword", HeaderValue::from_str(&config.password)?);
498
499    let resp = client
500        .post(&format!(
501            "{}://{}/rest/login",
502            match config.certificate {
503                Some(_) => "https",
504                None => "http",
505            },
506            config.endpoint
507        ))
508        .headers(headers)
509        .send()?
510        .error_for_status()?;
511
512    // We need a WSToken back from the server which takes the place of the
513    // password in future requests
514    let token = resp.headers().get("WStoken");
515    match token {
516        Some(data) => Ok(data.to_str()?.to_owned()),
517        None => Err(StorageError::new(format!(
518            "WSToken multiple lines. {:?}. Please check server",
519            token
520        ))),
521    }
522}
523
524// This is to delay the collections so the Brocade SAN switches do not
525// get their queue over-ran with requests until they can upgrade to newer version
526// which deals with that issue otherwise switch soft resets can occur
527// Added the 'use' statement here to be localized so this can all be removed later
528fn sleep_the_collections() {
529    use std::{thread, time};
530
531    let sleep_time = time::Duration::from_millis(5000);
532    let now = time::Instant::now();
533
534    thread::sleep(sleep_time);
535
536    assert!(now.elapsed() >= sleep_time);
537}
538
539impl Brocade {
540    // Deletes the client session
541    pub fn logout(&self) -> MetricsResult<()> {
542        let mut headers = HeaderMap::new();
543        headers.insert("WStoken", HeaderValue::from_str(&self.token)?);
544
545        self.client
546            .post(&format!(
547                "{}://{}/rest/logout",
548                match self.config.certificate {
549                    Some(_) => "https",
550                    None => "http",
551                },
552                self.config.endpoint
553            ))
554            .headers(headers)
555            .send()?
556            .error_for_status()?;
557        Ok(())
558    }
559
560    fn get_server_response<T>(&self, api_call: &str, ws_token: &str) -> MetricsResult<T>
561    where
562        T: DeserializeOwned + Debug,
563    {
564        let url = format!(
565            "{}://{}/rest/{}",
566            match self.config.certificate {
567                Some(_) => "https",
568                None => "http",
569            },
570            self.config.endpoint,
571            api_call
572        );
573        let resp = self
574            .client
575            .get(&url)
576            .header(
577                ACCEPT,
578                "application/vnd.brocade.networkadvisor+json;version=v1",
579            )
580            .header("WStoken", HeaderValue::from_str(ws_token)?)
581            .send()?
582            .error_for_status()?
583            .text()?;
584        trace!("server returned: {}", resp);
585        let json: Result<T, serde_json::Error> = serde_json::from_str(&resp);
586        trace!("json result: {:?}", json);
587        Ok(json?)
588    }
589
590    pub fn get_fc_fabrics(&self, t: DateTime<Utc>) -> MetricsResult<Vec<TsPoint>> {
591        sleep_the_collections();
592        let result =
593            self.get_server_response::<FcFabrics>("resourcegroups/All/fcfabrics", &self.token)?;
594        let mut points = result
595            .fc_fabrics
596            .iter()
597            .flat_map(|fabric| fabric.into_point(Some("brocade_fc_fabric"), true))
598            .collect::<Vec<TsPoint>>();
599        for point in &mut points {
600            point.timestamp = Some(t)
601        }
602        Ok(points)
603    }
604
605    pub fn get_fc_switch_timeseries(
606        &self,
607        switch_key: &str,
608        timeseries: TimeSeries,
609    ) -> MetricsResult<()> {
610        // TODO: Not sure if these performance metrics need to be enabled on the switches first
611        sleep_the_collections();
612        let _url = format!(
613            "resourcegroups/All/fcswitches/{}/{}?duration=360",
614            switch_key,
615            match timeseries {
616                TimeSeries::Fc(ts) => ts.to_string(),
617                TimeSeries::FcIp(ts) => ts.to_string(),
618            }
619        );
620        Ok(())
621    }
622
623    pub fn get_fc_fabric_timeseries(
624        &self,
625        fabric_key: &str,
626        timeseries: &FabricTimeSeries,
627    ) -> MetricsResult<()> {
628        // TODO: Not sure if these performance metrics need to be enabled on the switches first
629        sleep_the_collections();
630        let _url = format!(
631            "resourcegroups/All/fcfabrics/{}/{}?duration=360",
632            fabric_key,
633            timeseries.to_string(),
634        );
635
636        Ok(())
637    }
638
639    pub fn get_fc_fabric_ids(&self) -> MetricsResult<Vec<String>> {
640        sleep_the_collections();
641        let result = self
642            .get_server_response::<FcFabrics>("resourcegroups/All/fcfabrics", &self.token)
643            .map(|fabrics| {
644                let fabrics: Vec<String> = fabrics
645                    .fc_fabrics
646                    .iter()
647                    .map(|fabric| fabric.key.clone())
648                    .collect::<Vec<String>>();
649                fabrics
650            })?;
651        Ok(result)
652    }
653
654    pub fn get_fc_ports(&self, fabric_key: &str, t: DateTime<Utc>) -> MetricsResult<Vec<TsPoint>> {
655        sleep_the_collections();
656        let result = self.get_server_response::<FcPorts>(
657            &format!("resourcegroups/All/fcswitches/{}/fcports", fabric_key),
658            &self.token,
659        )?;
660        let mut points = result
661            .fc_ports
662            .iter()
663            .flat_map(|port| port.into_point(Some("brocade_fc_port"), true))
664            .collect::<Vec<TsPoint>>();
665        for point in &mut points {
666            point.timestamp = Some(t)
667        }
668        Ok(points)
669    }
670
671    pub fn get_fc_switch_ids(&self) -> MetricsResult<Vec<String>> {
672        sleep_the_collections();
673        let result = self
674            .get_server_response::<FcSwitches>("resourcegroups/All/fcswitches", &self.token)
675            .map(|switches| {
676                let switches: Vec<String> = switches
677                    .fc_switches
678                    .iter()
679                    .map(|switch| switch.key.clone())
680                    .collect::<Vec<String>>();
681                switches
682            })?;
683        Ok(result)
684    }
685
686    pub fn get_fc_switches(&self, t: DateTime<Utc>) -> MetricsResult<Vec<TsPoint>> {
687        sleep_the_collections();
688        let result =
689            self.get_server_response::<FcSwitches>("resourcegroups/All/fcswitches", &self.token)?;
690        let mut points = result
691            .fc_switches
692            .iter()
693            .flat_map(|switch| switch.into_point(Some("brocade_fc_switch"), true))
694            .collect::<Vec<TsPoint>>();
695        for point in &mut points {
696            point.timestamp = Some(t)
697        }
698        Ok(points)
699    }
700
701    pub fn get_resource_groups(&self) -> MetricsResult<ResourceGroups> {
702        let result = self.get_server_response::<ResourceGroups>("resourcegroups", &self.token)?;
703        Ok(result)
704    }
705}