Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cterasdk/edge/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
'shares',
'shell',
'smb',
'stats',
'support',
'sync',
'syslog',
Expand Down
65 changes: 65 additions & 0 deletions cterasdk/edge/stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import logging

from ..common import parse_base_object_ref
from ..exceptions import CTERAException
from .base_command import BaseCommand


logger = logging.getLogger('cterasdk.edge')


VALID_STAT_TYPES = ('cpu', 'memory', 'cache', 'volume', 'connections', 'local_io', 'disk_io', 'cloud_io')
VALID_INTERVALS = ('hour', 'day', 'week', 'month', 'year', 'last')


class Stats(BaseCommand):
"""
Edge Filer statistics via the HTTP Relay channel.

Uses the portal's RemoteDeviceServlet (/devices/<name>/) which forwards
all HTTP headers — including cookies — to the device, unlike the binary
API channel (/devicecmdnew/) which cannot carry cookies.
"""

def __init__(self, edge, relay):
super().__init__(edge)
self._relay = relay
self._authenticated = False

def get(self, stat_type, interval='hour'):
"""
Get device statistics

:param str stat_type: Statistic type.
Options: ``cpu``, ``memory``, ``cache``, ``volume``, ``connections``, ``local_io``, ``disk_io``, ``cloud_io``
:param str,optional interval: Time interval, defaults to ``hour``.
Options: ``hour``, ``day``, ``week``, ``month``, ``year``, ``last``
:returns: Statistics data
"""
if stat_type not in VALID_STAT_TYPES:
raise ValueError(f'Invalid stat_type {stat_type!r}. Valid: {VALID_STAT_TYPES}')
if interval not in VALID_INTERVALS:
raise ValueError(f'Invalid interval {interval!r}. Valid: {VALID_INTERVALS}')
self._ensure_session()
return self._relay.get(f'/stats/{stat_type}', params={'interval': interval})

def _ensure_session(self):
"""
Authenticate to the device through the relay channel on first use.

Retrieves an SSO token from the portal and uses it to log in to the
device via the relay tunnel. The device responds with a Set-Cookie
header (_cteraSessionId_) which the shared HTTP session stores
automatically, scoped to the relay path.
"""
if not self._authenticated:
Portal = self._edge._Portal # pylint: disable=protected-access
tenant = parse_base_object_ref(self._edge.portal).name
device_name = self._edge.name
logger.debug('Retrieving SSO ticket for relay session. %s', {'tenant': tenant, 'device': device_name})
token = Portal.api.execute(f'/portals/{tenant}/devices/{device_name}', 'singleSignOn')
if not token:
raise CTERAException('Failed to retrieve SSO ticket for relay session.')
self._relay.get('/ssologin', params={'ticket': token})
self._authenticated = True
logger.debug('Relay session established. %s', {'tenant': tenant, 'device': device_name})
19 changes: 17 additions & 2 deletions cterasdk/objects/synchronous/edge.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,17 @@
afp, aio, antivirus, array, audit, backup, cache, cli, config, connection, ctera_migrate,
dedup, directoryservice, drive, files, firmware, ftp, groups, licenses, login,
logs, mail, network, nfs, ntp, power, remote, rsync, ransom_protect, services,
shares, shell, smb, snmp, ssh, ssl, support, sync, syslog, tasks, telnet,
shares, shell, smb, snmp, ssh, ssl, stats, support, sync, syslog, tasks, telnet,
timezone, users, volumes,
)


class Clients:

def __init__(self, edge, Portal):
self._edge = edge
self._Portal = Portal
self._stats = None
if Portal:
edge._Portal = Portal
edge.default.close()
Expand All @@ -29,6 +32,14 @@ def __init__(self, edge, Portal):
self.api = edge.default.clone(clients.API, EndpointBuilder.new(edge.base, '/admingui/api'))
self.io = IO(edge)

@property
def stats(self):
if self._stats is None and self._Portal:
relay_base = EndpointBuilder.new(f'{self._Portal.ctera.baseurl}/devices/{self._edge.name}')
relay = self._Portal.default.clone(clients.API, relay_base, authenticator=lambda *_: True)
self._stats = stats.Stats(self._edge, relay)
return self._stats


class IO:

Expand Down Expand Up @@ -133,6 +144,10 @@ def api(self):
def io(self):
return self.clients.io

@property
def stats(self):
return self.clients.stats

@property
def _session_id_key(self):
return '_cteraSessionId_'
Expand Down Expand Up @@ -164,5 +179,5 @@ def _omit_fields(self):
return super()._omit_fields + ['afp', 'aio', 'array', 'audit', 'antivirus', 'backup', 'cache', 'cli', 'config', 'ctera_migrate',
'dedup', 'directoryservice', 'drive', 'files', 'firmware', 'ftp', 'groups', 'licenses', 'logs',
'mail', 'network', 'nfs', 'ntp', 'power', 'ransom_protect', 'rsync', 'services', 'shares', 'shell',
'smb', 'snmp', 'ssh', 'ssl', 'support', 'sync', 'syslog', 'tasks', 'telnet', 'timezone',
'smb', 'snmp', 'ssh', 'ssl', 'stats', 'support', 'sync', 'syslog', 'tasks', 'telnet', 'timezone',
'users', 'volumes']
71 changes: 71 additions & 0 deletions tests/ut/edge/test_stats.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from unittest import mock

from cterasdk.edge import stats
from cterasdk.exceptions import CTERAException
from tests.ut.edge import base_edge


class TestEdgeStats(base_edge.BaseEdgeTest):

def setUp(self):
super().setUp()
self._relay = mock.MagicMock()
self._portal = mock.MagicMock()
self._filer._Portal = self._portal
self._filer.portal = 'objs/21//TeamPortal/test_tenant'
self._filer.name = 'my-filer'
self._sso_token = 'sso-token-abc123'
self._portal.api.execute.return_value = self._sso_token
self._stats = stats.Stats(self._filer, self._relay)

def test_get_cpu_default_interval(self):
self._stats.get('cpu')
self._relay.get.assert_called_with('/stats/cpu', params={'interval': 'hour'})

def test_get_memory_with_interval(self):
self._stats.get('memory', interval='day')
self._relay.get.assert_called_with('/stats/memory', params={'interval': 'day'})

def test_get_all_stat_types(self):
for stat_type in stats.VALID_STAT_TYPES:
self._relay.reset_mock()
self._stats._authenticated = False
self._stats.get(stat_type, interval='hour')
self._relay.get.assert_called_with(f'/stats/{stat_type}', params={'interval': 'hour'})

def test_get_all_intervals(self):
for interval in stats.VALID_INTERVALS:
self._relay.reset_mock()
self._stats._authenticated = False
self._stats.get('cpu', interval=interval)
self._relay.get.assert_called_with('/stats/cpu', params={'interval': interval})

def test_invalid_stat_type_raises_value_error(self):
with self.assertRaises(ValueError):
self._stats.get('invalid_type')

def test_invalid_interval_raises_value_error(self):
with self.assertRaises(ValueError):
self._stats.get('cpu', interval='invalid_interval')

def test_ensure_session_retrieves_sso_token(self):
self._stats.get('cpu')
self._portal.api.execute.assert_called_once_with('/portals/test_tenant/devices/my-filer', 'singleSignOn')

def test_ensure_session_calls_ssologin(self):
self._stats.get('cpu')
self._relay.get.assert_any_call('/ssologin', params={'ticket': self._sso_token})

def test_ensure_session_called_once(self):
self._stats.get('cpu')
self._stats.get('memory', interval='day')
self._portal.api.execute.assert_called_once()

def test_ensure_session_raises_on_empty_token(self):
self._portal.api.execute.return_value = None
with self.assertRaises(CTERAException):
self._stats.get('cpu')

def test_stats_not_available_locally(self):
local_filer = base_edge.Edge("")
self.assertIsNone(local_filer.stats)
Loading