Skip to content

Commit

Permalink
23.10 Astra Control Release (#77)
Browse files Browse the repository at this point in the history
* adding retentionTime to bucket table output
* changing where protection tasks are created
* adding ACP installation
* changing command from "deploy helm" to "deploy chart"
* adding retry logic to backup/snapshot monitoring
  • Loading branch information
MichaelHaigh authored Nov 10, 2023
1 parent c231191 commit 683dca2
Show file tree
Hide file tree
Showing 13 changed files with 544 additions and 158 deletions.
1 change: 1 addition & 0 deletions astraSDK/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from . import common
from . import entitlements
from . import hooks
from . import k8s
from . import namespaces
from . import notifications
from . import protections
Expand Down
3 changes: 2 additions & 1 deletion astraSDK/backups.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def __init__(self, quiet=True, verbose=False, output="json"):

def main(self, appFilter=None):
if self.apps is False:
print("Call to getApps().main() failed")
if not self.quiet:
super().printError("Call to getApps() failed")
return False

"""self.apps = {"items":[{"appDefnSource":"namespace","appLabels":[],
Expand Down
4 changes: 2 additions & 2 deletions astraSDK/buckets.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ def main(self, nameFilter=None, provider=None):
dataReturn = yaml.dump(bucketsCooked)
elif self.output == "table":
dataReturn = self.basicTable(
["bucketID", "name", "credentialID", "provider", "state"],
["id", "name", "credentialID", "provider", "state"],
["bucketID", "name", "credentialID", "provider", "state", "retentionTime"],
["id", "name", "credentialID", "provider", "state", "retentionTime"],
bucketsCooked,
)
if not self.quiet:
Expand Down
87 changes: 49 additions & 38 deletions astraSDK/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"""

import json
import kubernetes
import os
import sys
import yaml
Expand Down Expand Up @@ -69,15 +70,11 @@ def __init__(self):
raise SystemExit(f"{item} is a required field in {configFile}")

if "." in self.conf.get("astra_project"):
self.base = "https://%s/accounts/%s/" % (
self.conf.get("astra_project"),
self.conf.get("uid"),
)
self.domain = self.conf.get("astra_project")
else:
self.base = "https://%s.astra.netapp.io/accounts/%s/" % (
self.conf.get("astra_project"),
self.conf.get("uid"),
)
self.domain = "%s.astra.netapp.io" % (self.conf.get("astra_project"))
self.account_id = self.conf.get("uid")
self.base = "https://%s/accounts/%s/" % (self.domain, self.account_id)
self.headers = self.conf.get("headers")

if self.conf.get("verifySSL") is False:
Expand All @@ -91,11 +88,50 @@ def main(self):
"base": self.base,
"headers": self.headers,
"verifySSL": self.verifySSL,
"domain": self.domain,
"account_id": self.account_id,
}


class SDKCommon:
class BaseCommon:
def __init__(self):
pass

def printError(self, ret):
"""Function to print relevant error information when a call fails"""
try:
sys.stderr.write(colored(json.dumps(json.loads(ret.text), indent=2), "red") + "\n")
except json.decoder.JSONDecodeError:
sys.stderr.write(colored(ret.text, "red"))
except AttributeError:
sys.stderr.write(colored(ret, "red"))

def recursiveGet(self, k, item):
"""Recursion function which is just a wrapper around dict.get(key), to handle cases
where there's a dict within a dict. A '.' in the key name ('metadata.creationTimestamp')
is used for identification purposes."""
if len(k.split(".")) > 1:
return self.recursiveGet(k.split(".", 1)[1], item[k.split(".")[0]])
else:
return item.get(k)

def basicTable(self, tabHeader, tabKeys, dataDict):
"""Function to create a basic tabulate table for terminal printing"""
tabData = []
for item in dataDict["items"]:
# Generate a table row based on the keys list
row = [self.recursiveGet(k, item) for k in tabKeys]
# Handle cases where table row has a nested list
for c, r in enumerate(row):
if type(r) is list:
row[c] = ", ".join(r)
tabData.append(row)
return tabulate(tabData, tabHeader, tablefmt="grid")


class SDKCommon(BaseCommon):
def __init__(self):
super().__init__()
self.conf = getConfig().main()
self.base = self.conf.get("base")
self.headers = self.conf.get("headers")
Expand Down Expand Up @@ -175,33 +211,8 @@ def printVerbose(self, url, method, headers, data, params):
print(colored(f"API data: {data}", "green"))
print(colored(f"API params: {params}", "green"))

def printError(self, ret):
"""Function to print relevant error information when a call fails"""
try:
sys.stderr.write(colored(json.dumps(json.loads(ret.text), indent=2), "red") + "\n")
except json.decoder.JSONDecodeError:
sys.stderr.write(colored(ret.text, "red"))
except AttributeError:
sys.stderr.write(colored(f"SDKCommon().printError: Unknown error: {ret}", "red"))

def recursiveGet(self, k, item):
"""Recursion function which is just a wrapper around dict.get(key), to handle cases
where there's a dict within a dict. A '.' in the key name ('metadata.creationTimestamp)
is used for identification purposes."""
if len(k.split(".")) > 1:
return self.recursiveGet(k.split(".", 1)[1], item[k.split(".")[0]])
else:
return item.get(k)

def basicTable(self, tabHeader, tabKeys, dataDict):
"""Function to create a basic tabulate table for terminal printing"""
tabData = []
for item in dataDict["items"]:
# Generate a table row based on the keys list
row = [self.recursiveGet(k, item) for k in tabKeys]
# Handle cases where table row has a nested list
for c, r in enumerate(row):
if type(r) is list:
row[c] = ", ".join(r)
tabData.append(row)
return tabulate(tabData, tabHeader, tablefmt="grid")
class KubeCommon(BaseCommon):
def __init__(self):
super().__init__()
self.kube_config = kubernetes.config.load_kube_config()
215 changes: 215 additions & 0 deletions astraSDK/k8s.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""
Copyright 2023 NetApp, Inc
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.
"""

import base64
import copy
import json
import kubernetes
import yaml

from .common import KubeCommon, SDKCommon


class getResources(KubeCommon):
def __init__(self, quiet=True, output="json"):
"""quiet: Will there be CLI output or just return (datastructure)
output: json: (default) output in JSON
yaml: output in yaml"""
self.quiet = quiet
self.output = output
super().__init__()

def main(
self,
plural,
version="v1",
group="trident.netapp.io",
keyFilter=None,
valFilter=None,
):
with kubernetes.client.ApiClient(self.kube_config) as api_client:
api_instance = kubernetes.client.CustomObjectsApi(api_client)
try:
resp = api_instance.list_cluster_custom_object(
group=group,
version=version,
plural=plural,
)
if keyFilter and valFilter:
filterCopy = copy.deepcopy(resp)
for counter, r in enumerate(filterCopy.get("items")):
if self.recursiveGet(keyFilter, r) != valFilter:
resp["items"].remove(filterCopy["items"][counter])

if self.output == "yaml":
resp = yaml.dump(resp)

if not self.quiet:
print(json.dumps(resp) if type(resp) is dict else resp)
return resp

except kubernetes.client.rest.ApiException as e:
self.printError(e)


class updateResource(KubeCommon):
def __init__(self, quiet=True):
"""quiet: Will there be CLI output or just return (datastructure)"""
self.quiet = quiet
super().__init__()

def main(
self,
plural,
name,
body,
version="v1",
group="trident.netapp.io",
):
with kubernetes.client.ApiClient(self.kube_config) as api_client:
api_instance = kubernetes.client.CustomObjectsApi(api_client)
try:
resp = api_instance.patch_cluster_custom_object(group, version, plural, name, body)

if not self.quiet:
print(json.dumps(resp) if type(resp) is dict else resp)
return resp

except kubernetes.client.rest.ApiException as e:
self.printError(e)


class getNamespaces(KubeCommon):
def __init__(self, quiet=True, output="json"):
"""quiet: Will there be CLI output or just return (datastructure)
output: json: (default) output in JSON
yaml: output in yaml"""
self.quiet = quiet
self.output = output
super().__init__()

def main(self):
with kubernetes.client.ApiClient(self.kube_config) as api_client:
api_instance = kubernetes.client.CoreV1Api(api_client)
try:
resp = api_instance.list_namespace().to_dict()
systemNS = [
"astra-connector-operator",
"kube-node-lease",
"kube-public",
"kube-system",
"neptune-system",
"trident",
]
namespaces = copy.deepcopy(resp)
for counter, ns in enumerate(namespaces.get("items")):
if ns.get("metadata").get("name") in systemNS:
resp["items"].remove(namespaces["items"][counter])

if self.output == "yaml":
resp = yaml.dump(resp)

if not self.quiet:
print(json.dumps(resp, default=str) if type(resp) is dict else resp)
return resp

except kubernetes.client.rest.ApiException as e:
self.printError(e)


class getSecrets(KubeCommon):
def __init__(self, quiet=True, output="json"):
"""quiet: Will there be CLI output or just return (datastructure)
output: json: (default) output in JSON
yaml: output in yaml"""
self.quiet = quiet
self.output = output
super().__init__()

def main(self, namespace="trident"):
with kubernetes.client.ApiClient(self.kube_config) as api_client:
api_instance = kubernetes.client.CoreV1Api(api_client)
try:
resp = api_instance.list_namespaced_secret(namespace).to_dict()

if self.output == "yaml":
resp = yaml.dump(resp)

if not self.quiet:
print(json.dumps(resp, default=str) if type(resp) is dict else resp)
return resp

except kubernetes.client.rest.ApiException as e:
self.printError(e)


class createRegCred(KubeCommon, SDKCommon):
def __init__(self, quiet=True):
"""quiet: Will there be CLI output or just return (datastructure)"""
self.quiet = quiet
super().__init__()

def main(self, name=None, registry=None, username=None, password=None, namespace="trident"):
if (not username and password) or (username and not password):
raise SystemExit(
"Either both or neither of (username and password) should be specified"
)
if not registry:
registry = f"cr.{self.conf['domain']}"
if not username and not password:
username = self.conf["account_id"]
password = self.conf["headers"].get("Authorization").split(" ")[-1]

regCred = {
"auths": {
registry: {
"username": username,
"password": password,
"auth": base64.b64encode(f"{username}:{password}".encode("utf-8")).decode(
"utf-8"
),
}
}
}
regCredSecret = kubernetes.client.V1Secret(
metadata=(
kubernetes.client.V1ObjectMeta(name=name)
if name
else kubernetes.client.V1ObjectMeta(
generate_name="-".join(registry.split(".")) + "-"
)
),
type="kubernetes.io/dockerconfigjson",
data={
".dockerconfigjson": base64.b64encode(json.dumps(regCred).encode("utf-8")).decode(
"utf-8"
)
},
)

with kubernetes.client.ApiClient(self.kube_config) as api_client:
api_instance = kubernetes.client.CoreV1Api(api_client)
try:
resp = api_instance.create_namespaced_secret(
namespace=namespace,
body=regCredSecret,
).to_dict()
if not self.quiet:
print(json.dumps(resp, default=str) if type(resp) is dict else resp)
return resp
except kubernetes.client.rest.ApiException as e:
self.printError(e)
3 changes: 2 additions & 1 deletion astraSDK/snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ def __init__(self, quiet=True, verbose=False, output="json"):

def main(self, appFilter=None):
if self.apps is False:
print("Call to getApps() failed")
if not self.quiet:
super().printError("Call to getApps() failed")
return False

snaps = {}
Expand Down
Loading

0 comments on commit 683dca2

Please sign in to comment.