Skip to content

Commit

Permalink
feat(akamai): implement cloudlets v3 (refs #13)
Browse files Browse the repository at this point in the history
  • Loading branch information
ynohat committed Nov 29, 2021
1 parent f60a73a commit 842436d
Show file tree
Hide file tree
Showing 22 changed files with 1,463 additions and 55 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,4 @@ dmypy.json
.pyre/
.vscode
*.code-workspace
.bossmancache
9 changes: 9 additions & 0 deletions bossman/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,15 @@ def getenv(key, default=None):
"switch_key": getenv("AKAMAI_EDGERC_SWITCHKEY", None),
}
},
{
"module": "bossman.plugins.akamai.cloudlet_v3",
"pattern": "akamai/cloudlet/{name}",
"options": {
"edgerc": getenv("AKAMAI_EDGERC", expanduser("~/.edgerc")),
"section": getenv("AKAMAI_EDGERC_SECTION", "default"),
"switch_key": getenv("AKAMAI_EDGERC_SWITCHKEY", None),
}
},
)

class PathPatternMatch:
Expand Down
3 changes: 3 additions & 0 deletions bossman/plugins/akamai/cloudlet_v3/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .client import *
from .data import *
from .resourcetype import *
111 changes: 111 additions & 0 deletions bossman/plugins/akamai/cloudlet_v3/client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
from datetime import datetime
import dateutil.parser
from typing import List
from cattr import Converter

from bossman.logging import get_class_logger
from bossman.plugins.akamai.lib.edgegrid import Session
from .data import *
from .error import *

class CloudletAPIV3Client:
def __init__(self, edgerc, section, switch_key=None, **kwargs):
self.logger = get_class_logger(self)
self.session = Session(edgerc, section, switch_key=switch_key, **kwargs)
self.converter = Converter()
self.converter.register_structure_hook(datetime, lambda d, t: dateutil.parser.isoparse(d))

def get_policy_by_name(self, name):
self.logger.debug("get_policy_by_name name={name}".format(name=name))
id = None
policies = self.get_policies()
for policy in policies:
if policy.name == name:
return policy
raise PolicyNameNotFound(name)

def get_policies(self) -> List[SharedPolicy]:
self.logger.debug("get_policies")
response = self.session.get("/cloudlets/v3/policies")
if response.status_code != 200:
raise CloudletAPIV3Error(response.json())
# import json
# print(json.dumps(response.json(), indent=2))
response = self.converter.structure(response.json(), GetPoliciesResponse)
return response.content

def get_policy_version(self, policyId: int, policyVersion: int) -> SharedPolicyVersion:
self.logger.debug("get_policy_version policyId={policyId}, policyVersion={policyVersion}".format(policyId=policyId, policyVersion=policyVersion))
response = self.session.get("/cloudlets/v3/policies/{policyId}/versions/{policyVersion}".format(policyId=policyId, policyVersion=policyVersion))
if response.status_code != 200:
raise CloudletAPIV3Error(response.json())
return self.converter.structure(response.json(), SharedPolicyVersion)

def get_latest_policy_version(self, policyId: int) -> Optional[SharedPolicyVersion]:
self.logger.debug("get_policy_version policyId={policyId}".format(policyId=policyId))
response = self.session.get("/cloudlets/v3/policies/{policyId}/versions".format(policyId=policyId), params=dict(size=10))
if response.status_code != 200:
raise CloudletAPIV3Error(response.json())
policy_versions = self.converter.structure(response.json(), GetPolicyVersionsResponse)
if len(policy_versions.content):
return policy_versions.content[0]
return None

def get_latest_policy_versions(self, policyId: int, count: int) -> List[SharedPolicyVersion]:
self.logger.debug("get_latest_policy_versions policyId={policyId}, count={count}".format(policyId=policyId, count=count))
response = self.session.get("/cloudlets/v3/policies/{policyId}/versions".format(policyId=policyId), params=dict(size=count))
if response.status_code != 200:
raise CloudletAPIV3Error(response.json())
policy_versions = self.converter.structure(response.json(), GetPolicyVersionsResponse)
return policy_versions.content

def create_policy(self, policyName: str, description: str, cloudletType: SharedPolicyCloudletType, groupId: int) -> SharedPolicy:
self.logger.debug("create_policy policyName={policyName}".format(policyName=policyName))
response = self.session.post("/cloudlets/v3/policies", json=dict(
name=policyName,
cloudletType=cloudletType.value,
groupId=groupId,
description=description,
))
if response.status_code != 201:
raise CloudletAPIV3Error(response.json())
return self.converter.structure(response.json(), SharedPolicy)

def update_policy(self, policyId: int, description: str, groupId: int) -> SharedPolicy:
self.logger.debug("update_policy policyName={policyId}".format(policyId=policyId))
response = self.session.put("/cloudlets/v3/policies/{policyId}".format(policyId=policyId), json=dict(
groupId=groupId,
description=description,
))
if response.status_code != 202:
raise CloudletAPIV3Error(response.json())
return self.converter.structure(response.json(), SharedPolicy)

def create_policy_version(self, policyId: int, description: str, matchRules: List[dict]) -> SharedPolicyVersion:
self.logger.debug("create_policy_version policyId={policyId}".format(policyId=policyId))
response = self.session.post("/cloudlets/v3/policies/{policyId}/versions".format(policyId=policyId), json=dict(
description=description,
matchRules=matchRules,
))
if response.status_code != 201:
raise CloudletAPIV3Error(response.json())
return self.converter.structure(response.json(), SharedPolicyVersion)

def activate_policy_version(self, policyId: int, policyVersion: int, network: Network) -> SharedPolicyActivation:
self.logger.debug("activate_policy_version policyId={policyId}, policyVersion={policyVersion}".format(policyId=policyId, policyVersion=policyVersion))
response = self.session.post("/cloudlets/v3/policies/{policyId}/activations".format(policyId=policyId), json=dict(
network=network.value,
operation=SharedPolicyActivationOperation.ACTIVATION.value,
policyVersion=policyVersion,
))
if response.status_code != 202:
result = response.json().get('errors')[0]
raise CloudletAPIV3Error(result.get('detail'))
return self.converter.structure(response.json(), SharedPolicyActivation)

def get_policy_version_activation_status(self, activation: SharedPolicyActivation) -> SharedPolicyActivation:
self.logger.debug("get_policy_version_activation_status policyId={policyId}, activationId={activationId}".format(policyId=activation.policyId, activationId=activation.id))
response = self.session.get("/cloudlets/v3/policies/{policyId}/activations/{activationId}".format(policyId=activation.policyId, activationId=activation.id))
if response.status_code != 200:
raise CloudletAPIV3Error(response.json())
return self.converter.structure(response.json(), SharedPolicyActivation)
102 changes: 102 additions & 0 deletions bossman/plugins/akamai/cloudlet_v3/data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import re
from dataclasses import dataclass
from datetime import datetime
from enum import Enum
from typing import List, Optional



class Network(Enum):
PRODUCTION = 'PRODUCTION'
STAGING = 'STAGING'

@property
def alias(self) -> str:
return re.sub("[aeiou]", "", self.value, flags=re.IGNORECASE)[:3].upper()

@property
def color(self) -> str:
return "green" if self == Network.PRODUCTION else "magenta"

class SharedPolicyActivationOperation(Enum):
ACTIVATION = 'ACTIVATION'
DEACTIVATION = 'DEACTIVATION'

class SharedPolicyActivationStatus(Enum):
IN_PROGRESS = 'IN_PROGRESS'
SUCCESS = 'SUCCESS'
FAILED = 'FAILED'

class SharedPolicyPolicyType(Enum):
SHARED = 'SHARED'

class SharedPolicyCloudletType(Enum):
ER = 'ER'
FR = 'FR'
AS = 'AS'

@dataclass
class SharedPolicyActivation:
id: int
createdBy: str
createdDate: datetime
finishDate: Optional[datetime]
network: Network
operation: SharedPolicyActivationOperation
status: SharedPolicyActivationStatus
policyId: int
policyVersion: int
policyVersionDeleted: bool

@dataclass
class SharedPolicyCurrentActivationsNetwork:
effective: Optional[SharedPolicyActivation] = None
latest: Optional[SharedPolicyActivation] = None

@dataclass
class SharedPolicyCurrentActivations:
production: SharedPolicyCurrentActivationsNetwork = None
staging: SharedPolicyCurrentActivationsNetwork = None

@dataclass
class SharedPolicy:
id: int
name: str
description: str
policyType: SharedPolicyPolicyType
cloudletType: SharedPolicyCloudletType
createdBy: str
createdDate: datetime
currentActivations: SharedPolicyCurrentActivations
groupId: int
modifiedBy: str
modifiedDate: datetime

@dataclass
class GetPoliciesResponse:
content: List[SharedPolicy]

@dataclass
class SharedPolicyVersion:
policyId: int
version: int
createdBy: str
createdDate: datetime
modifiedBy: str
modifiedDate: datetime
description: str
matchRules: Optional[List[dict]] = None

@dataclass
class GetPolicyVersionsResponse:
content: List[SharedPolicyVersion]

@dataclass
class SharedPolicyAsCode:
"""
Subset of SharedPolicy that must be versioned in order to support CRUD.
"""
description: str
groupId: int
cloudletType: SharedPolicyCloudletType
matchRules: Optional[List[dict]] = None
13 changes: 13 additions & 0 deletions bossman/plugins/akamai/cloudlet_v3/error.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from bossman.errors import BossmanError

class CloudletAPIV3Error(BossmanError):
pass

class PolicyNameNotFound(CloudletAPIV3Error):
pass

class PolicyVersionValidationError(CloudletAPIV3Error):
pass

class PolicyActivationAlreadyPendingError(CloudletAPIV3Error):
pass
24 changes: 24 additions & 0 deletions bossman/plugins/akamai/cloudlet_v3/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pathlib
from bossman.abc import ResourceABC

class SharedPolicyResource(ResourceABC):
def __init__(self, path, **kwargs):
super(SharedPolicyResource, self).__init__(path)
self.__name = kwargs.get("name")

@property
def name(self):
return self.__name

@property
def policy_path(self):
# All operations use unix-style paths; this is important
return str(pathlib.PurePosixPath(self.path) / "policy.json")

@property
def paths(self):
return (self.policy_path,)

def __rich__(self):
prefix = self.path.replace(self.name, "")
return "[grey53]{}[/][yellow]{}[/]".format(prefix, self.name)
Loading

0 comments on commit 842436d

Please sign in to comment.