Skip to content

Commit 55d12f0

Browse files
authored
Saimon/create array with all drives (#124)
Features: 1) Create an array using all available drives if no drive was specified 2) Support modfiying the Portal's time zone 3) List templates by name 4) Configure template auto assignment policy 5) Support managing the Portal's SSL certificate (import, export) 6) Support SNMP configuration APIs, unittests and docs Technical improvements: * Add docs for auto template assignment, SSL certificates and timezone * Use from X import Y when needed * Use rsplit with maxsplit 1 when getting the last value * Use "with" when reading from files * __all__ definitions - no need to map str method to strings
1 parent ac08216 commit 55d12f0

File tree

19 files changed

+709
-21
lines changed

19 files changed

+709
-21
lines changed

cterasdk/core/enum.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,3 +459,26 @@ class Platform:
459459
Linux = 'LinuxX86'
460460
Windows = 'WindowsX86'
461461
OSX = 'OSxX86'
462+
463+
464+
class TemplateCriteria:
465+
"""
466+
Configuration Template Auto Assignment Rule Builder Criterias
467+
468+
:ivar str Type: Device type
469+
:ivar str OperatingSystem: Operating system
470+
:ivar str Version: Installed software version
471+
:ivar str Hostname: Hostname
472+
:ivar str Name: Device name
473+
:ivar str Owner: Device owner username
474+
:ivar str Plan: Plan name
475+
:ivar str Groups: Device owner local or domain groups
476+
"""
477+
Type = 'DeviceType'
478+
OperatingSystem = 'OperatingSystem'
479+
Version = 'InstalledSoftwareVersion'
480+
Hostname = 'Hostname'
481+
Name = 'DeviceName'
482+
Owner = 'OwnerUsername'
483+
Plan = 'Plan'
484+
Groups = 'ownerGroups'

cterasdk/core/settings.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import logging
2+
3+
from .base_command import BaseCommand
4+
5+
6+
class Settings(BaseCommand):
7+
"""
8+
Portal Settings APIs
9+
10+
:ivar cterasdk.core.settings.GlobalSettings global_settings: Object holding the Portal Global Settings APIs
11+
"""
12+
13+
def __init__(self, portal):
14+
super().__init__(portal)
15+
self.global_settings = GlobalSettings(self._portal)
16+
17+
18+
class GlobalSettings(BaseCommand):
19+
20+
def get_timezone(self):
21+
"""
22+
Get timezone
23+
"""
24+
return self._portal.get('/settings/timezone')
25+
26+
def set_timezone(self, timezone):
27+
"""
28+
Set timezone
29+
30+
:param str timezone: Timezone
31+
"""
32+
logging.getLogger().info('Updating timezone. %s', {'timezone': timezone})
33+
response = self._portal.put('/settings/timezone', timezone)
34+
logging.getLogger().info('Updated timezone. %s', {'timezone': timezone})
35+
return response

cterasdk/core/ssl.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import logging
2+
from zipfile import ZipFile
3+
4+
from .base_command import BaseCommand
5+
from ..lib import FileSystem, X509Certificate, TempfileServices, create_certificate_chain
6+
7+
8+
class SSL(BaseCommand):
9+
"""
10+
Portal SSL Certificate APIs
11+
"""
12+
13+
def __init__(self, portal):
14+
super().__init__(portal)
15+
self._filesystem = FileSystem.instance()
16+
17+
def get(self):
18+
"""
19+
Retrieve details of the current installed SSL certificate
20+
21+
:return cterasdk.common.object.Object: An object including the SSL certificate details
22+
"""
23+
logging.getLogger().info('Retrieving SSL certificate')
24+
response = self._portal.get('/settings/ca')
25+
logging.getLogger().info('Retrieved SSL certificate')
26+
return response
27+
28+
@property
29+
def thumbprint(self):
30+
"""
31+
Get the SHA1 thumbprint of the Portal SSL certificate
32+
"""
33+
return self.get().thumbprint
34+
35+
def export(self, destination=None):
36+
"""
37+
Export the Portal SSL Certificate to a ZIP archive
38+
39+
:param str,optional destination:
40+
File destination, defaults to the default directory
41+
"""
42+
directory, filename = self._filesystem.split_file_directory_with_defaults(destination, 'certificate.zip')
43+
logging.getLogger().info('Exporting SSL certificate.')
44+
handle = self._portal.openfile('/admin/preview/exportCertificate', use_file_url=True)
45+
filepath = self._filesystem.save(directory, filename, handle)
46+
logging.getLogger().info('Exported SSL certificate. %s', {'filepath': filepath})
47+
return filepath
48+
49+
def create_zip_archive(self, private_key, *certificates):
50+
"""
51+
Create a ZIP archive that can be imported to CTERA Portal
52+
53+
:param str private_key: A path to the PEM-encoded private key file
54+
:param list[str] certificates: A list of paths of the PEM-encoded certificate files
55+
"""
56+
tempdir = TempfileServices.mkdir()
57+
58+
key_basename = 'private.key'
59+
private_keyfile = self._filesystem.copyfile(private_key, FileSystem.join(tempdir, key_basename))
60+
61+
cert_basename = 'certificate'
62+
certificates = [X509Certificate.from_file(certificate) for certificate in certificates]
63+
certificate_chain = create_certificate_chain(*certificates)
64+
65+
certificate_chain_zip_archive = None
66+
if certificate_chain:
67+
certificate_chain_zip_archive = FileSystem.join(tempdir, '{}.zip'.format(cert_basename))
68+
with ZipFile(certificate_chain_zip_archive, 'w') as zip_archive:
69+
zip_archive.write(private_keyfile, key_basename)
70+
for idx, certificate in enumerate(certificate_chain):
71+
filename = '{}{}.crt'.format(cert_basename, idx if idx > 0 else '')
72+
filepath = FileSystem.join(tempdir, filename)
73+
self._filesystem.write(filepath, certificate.pem_data)
74+
zip_archive.write(filepath, filename)
75+
76+
return certificate_chain_zip_archive
77+
78+
def import_from_zip(self, zipfile):
79+
"""
80+
Import an SSL Certificate to CTERA Portal from a ZIP archive
81+
82+
:param str zipfile: A zip archive including the private key and SSL certificate chain
83+
"""
84+
return self._import_certificate(zipfile)
85+
86+
def import_from_chain(self, private_key, *certificates):
87+
"""
88+
Import an SSL Certificate to CTERA Portal from a chain
89+
90+
:param str private_key: A path to the PEM-encoded private key file
91+
:param list[str] certificates: A list of paths to the PEM-encoded certificates
92+
"""
93+
zipflie = self.create_zip_archive(private_key, *certificates)
94+
return self.import_from_zip(zipflie)
95+
96+
def _import_certificate(self, zipfile):
97+
info = self._filesystem.get_local_file_info(zipfile)
98+
logging.getLogger().info('Uploading SSL certificate.')
99+
with open(zipfile, 'rb') as fd:
100+
response = self._portal.upload(
101+
'/settings/importCertificate',
102+
dict(
103+
name='upload',
104+
certificate=(info['name'], fd, info['mimetype'][0])
105+
)
106+
)
107+
if not isinstance(response, str):
108+
logging.getLogger().error('Failed uploading SSL certificate. %s', {'reason': response.msg})
109+
logging.getLogger().info('Uploaded SSL certificate.')
110+
self._portal.startup.wait()

cterasdk/core/templates.py

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import logging
22

3-
from ..common import union, parse_base_object_ref, ApplicationBackupSet, Object
3+
from ..common import union, parse_base_object_ref, ApplicationBackupSet, PolicyRuleConverter, Object
44
from ..exception import CTERAException
55
from .base_command import BaseCommand
66
from . import query
@@ -113,15 +113,35 @@ def _create_template_firmware(platform, base_object_ref):
113113
param.firmware = base_object_ref
114114
return param
115115

116-
def list_templates(self, include=None):
116+
def by_name(self, names, include=None):
117+
"""
118+
Get Templates by their names
119+
120+
:param list[str],optional names: List of names of templates
121+
:param list[str],optional include: List of fields to retrieve, defaults to ['name']
122+
:param list[cterasdk.core.query.FilterBuilder],optional filters: List of additional filters, defaults to None
123+
124+
:return: Iterator for all matching Templates
125+
:rtype: cterasdk.lib.iterator.Iterator
126+
"""
127+
filters = [query.FilterBuilder('name').eq(name) for name in names]
128+
return self.list_templates(include, filters)
129+
130+
def list_templates(self, include=None, filters=None):
117131
"""
118132
List Configuration Templates.\n
119133
To retrieve templates, you must first browse the tenant, using: `GlobalAdmin.portals.browse()`
120134
121135
:param list[str],optional include: List of fields to retrieve, defaults to ``['name']``
136+
:param list[],optional filters: List of additional filters, defaults to None
122137
"""
123138
include = union(include or [], Templates.default)
124-
param = query.QueryParamBuilder().include(include).build()
139+
builder = query.QueryParamBuilder().include(include)
140+
if filters:
141+
for query_filter in filters:
142+
builder.addFilter(query_filter)
143+
builder.orFilter((len(filters) > 1))
144+
param = builder.build()
125145
return query.iterator(self._portal, '/deviceTemplates', param)
126146

127147
def delete(self, name):
@@ -168,6 +188,51 @@ def remove_default(self, name, wait=False):
168188

169189
class TemplateAutoAssignPolicy(BaseCommand):
170190

191+
def get_policy(self):
192+
"""
193+
Get templates auto assignment policy
194+
"""
195+
return self._portal.execute('', 'getAutoAssignmentRules')
196+
197+
def set_policy(self, rules, apply_default=None, default=None, apply_changes=True):
198+
"""
199+
Set templates auto assignment policy
200+
201+
:param list[cterasdk.common.types.PolicyRule] rules: List of policy rules
202+
:param bool,optional apply_default: If no match found, apply default template. If not passed, the current config will be kept
203+
:param str,optional default: Name of a template to assign if no match found. Ignored unless the ``apply_default`` is set to ``True``
204+
:param bool,optional apply_changes: Apply changes upon update, defaults to ``True``
205+
"""
206+
templates = {rule.assignment for rule in rules}
207+
if default:
208+
templates.add(default)
209+
templates = list(templates)
210+
portal_templates = {template.name: template for template in self._portal.templates.by_name(templates, ['baseObjectRef'])}
211+
212+
not_found = [template for template in templates if template not in portal_templates.keys()]
213+
if not_found:
214+
logging.getLogger().error('Could not find one or more templates. %s', {'templates': not_found})
215+
raise CTERAException('Could not find one or more templates', None, templates=not_found)
216+
217+
policy = self.get_policy()
218+
219+
if apply_default is False:
220+
policy.defaultTemplate = None
221+
elif apply_default is True and default:
222+
policy.defaultTemplate = portal_templates.get(default).baseObjectRef
223+
224+
policy_rules = [PolicyRuleConverter.convert(rule, 'DeviceTemplateAutoAssignmentRule', 'template',
225+
portal_templates.get(rule.assignment).baseObjectRef) for rule in rules]
226+
policy.deviceTemplatesAutoAssignmentRules = policy_rules
227+
228+
response = self._portal.execute('', 'setAutoAssignmentRules', policy)
229+
logging.getLogger().info('Set templates auto assignment rules.')
230+
231+
if apply_changes:
232+
self.apply_changes(True)
233+
234+
return response
235+
171236
def apply_changes(self, wait=False):
172237
"""
173238
Apply provisioning changes.\n

cterasdk/core/types.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from collections import namedtuple
33
from ..common import DateTimeUtils, StringCriteriaBuilder, ListCriteriaBuilder, Object
44

5-
from .enum import PortalAccountType, CollaboratorType, FileAccessMode, PlanCriteria, BucketType, LocationType
5+
from .enum import PortalAccountType, CollaboratorType, FileAccessMode, PlanCriteria, TemplateCriteria, BucketType, LocationType
66

77

88
CloudFSFolderFindingHelper = namedtuple('CloudFSFolderFindingHelper', ('name', 'owner'))
@@ -230,6 +230,43 @@ def comment():
230230
return StringCriteriaBuilder(PlanCriteriaBuilder.Type, PlanCriteria.Comment)
231231

232232

233+
class TemplateCriteriaBuilder:
234+
235+
Type = 'DeviceCriteria'
236+
237+
@staticmethod
238+
def type():
239+
return ListCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Type)
240+
241+
@staticmethod
242+
def os():
243+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.OperatingSystem)
244+
245+
@staticmethod
246+
def version():
247+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Version)
248+
249+
@staticmethod
250+
def hostname():
251+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Hostname)
252+
253+
@staticmethod
254+
def name():
255+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Name)
256+
257+
@staticmethod
258+
def owner():
259+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Owner)
260+
261+
@staticmethod
262+
def plan():
263+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Plan)
264+
265+
@staticmethod
266+
def groups():
267+
return StringCriteriaBuilder(TemplateCriteriaBuilder.Type, TemplateCriteria.Groups)
268+
269+
233270
class Bucket:
234271

235272
def __init__(self, bucket, driver):

cterasdk/edge/array.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,23 @@ class Array(BaseCommand):
1111
def get(self, name=None):
1212
"""
1313
Get Array. If an array name was not passed as an argument, a list of all arrays will be retrieved
14+
1415
:param str,optional name: Name of the array
1516
"""
1617
return self._gateway.get('/config/storage/arrays' + ('' if name is None else ('/' + name)))
1718

18-
def add(self, array_name, level, members):
19+
def add(self, array_name, level, members=None):
1920
"""
2021
Add a new array
2122
2223
:param str array_name: Name for the new array
2324
:param RAIDLevel level: RAID level
24-
:param list(str) members: Members of the array
25+
:param list(str) members: Members of the array. If not specified, the system will try to create an array using all available drives
2526
"""
2627
param = Object()
2728
param.name = array_name
2829
param.level = level
29-
param.members = members
30+
param.members = [drive.name for drive in self._gateway.drive.get_status()] if members is None else members
3031

3132
try:
3233
logging.getLogger().info("Creating a storage array.")

cterasdk/edge/config.py

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,7 @@ def export(self, destination=None):
5555
File destination, defaults to the default directory
5656
"""
5757
default_filename = self._gateway.host() + datetime.now().strftime('_%Y-%m-%dT%H_%M_%S') + '.xml'
58-
directory = filename = None
59-
if destination:
60-
directory, filename = self._filesystem.split_file_directory(destination)
61-
if not filename:
62-
filename = default_filename
63-
else:
64-
directory = self._filesystem.get_dirpath()
65-
filename = default_filename
58+
directory, filename = self._filesystem.split_file_directory_with_defaults(destination, default_filename)
6659
logging.getLogger().info('Exporting configuration. %s', {'host': self._gateway.host()})
6760
handle = self._gateway.openfile('/export')
6861
filepath = FileSystem.instance().save(directory, filename, handle)

0 commit comments

Comments
 (0)