Files
awx/awxkit/awxkit/api/pages/api.py
2020-06-09 13:54:05 -04:00

286 lines
9.4 KiB
Python

import itertools
from awxkit.api.resources import resources
import awxkit.exceptions as exc
from . import base
from . import page
from ..mixins import has_create
EXPORTABLE_RESOURCES = [
'users',
'organizations',
'teams',
'credential_types',
'credentials',
'notification_templates',
'projects',
'inventory',
'job_templates',
'workflow_job_templates',
]
EXPORTABLE_RELATIONS = [
'Roles',
'NotificationTemplates',
]
EXPORTABLE_DEPENDENT_OBJECTS = [
'Labels',
'SurveySpec',
'WorkflowJobTemplateNodes',
]
NATURAL_KEYS = {
'user': ('username',),
'organization': ('name',),
'team': ('organization', 'name'),
'credential_type': ('name', 'kind'),
'credential': ('organization', 'name', 'credential_type'),
'notification_template': ('organization', 'name'),
'project': ('organization', 'name'),
'inventory': ('organization', 'name'),
'job_template': ('organization', 'name'),
'workflow_job_template': ('organization', 'name'),
# related resources
'role': ('name', ':content_object'),
'notification_template': ('organization', 'name'),
'label': ('organization', 'name'), # FIXME: label will need to be fully constructed from this
'workflow_job_template_node': ('workflow_job_template', 'identifier'),
}
def get_natural_key(pg):
natural_key = {'type': pg['type']}
lookup = NATURAL_KEYS.get(pg['type'], ())
for key in lookup or ():
if key.startswith(':'):
# treat it like a special-case related object
related_objs = [
related for name, related in pg.related.items()
if name not in ('users', 'teams')
]
if related_objs:
natural_key[key[1:]] = get_natural_key(related_objs[0].get())
elif key in pg.related:
natural_key[key] = get_natural_key(pg.related[key].get())
elif key in pg:
natural_key[key] = pg[key]
if not natural_key:
return None
return natural_key
def freeze(key):
if key is None:
return None
return frozenset((k, freeze(v) if isinstance(v, dict) else v) for k, v in key.items())
class Api(base.Base):
pass
page.register_page(resources.api, Api)
class ApiV2(base.Base):
# Common import/export methods
def _get_options(self, endpoint):
return endpoint.options().json['actions'].get('POST', {})
# Export methods
def _serialize_asset(self, asset, options):
# Drop any (credential_type) assets that are being managed by the Tower instance.
if asset.json.get('managed_by_tower'):
return None
try:
# Note: doing asset[key] automatically parses json blob strings, which can be a problem.
fields = {
key: asset.json[key] for key in options
if key in asset.json and key not in asset.related and key != 'id'
}
fields['natural_key'] = get_natural_key(asset)
fk_fields = {
key: get_natural_key(asset.related[key].get()) for key in options
if key in asset.related
}
related = {}
for key, related_endpoint in asset.related.items():
if key in asset.json or not related_endpoint:
continue
if key == 'object_roles':
continue
rel = related_endpoint._create()
if rel.__class__.__name__ in EXPORTABLE_RELATIONS:
by_natural_key = True
elif rel.__class__.__name__ in EXPORTABLE_DEPENDENT_OBJECTS:
by_natural_key = False
else:
continue
data = related_endpoint.get(all_pages=True)
if 'results' in data:
related_options = self._get_options(related_endpoint)
related[key] = [
get_natural_key(x) if by_natural_key else self._serialize_asset(x, related_options)
for x in data.results
]
else:
related[key] = data.json
except exc.Forbidden:
return None
related_fields = {'related': related} if related else {}
fields.update(fk_fields)
fields.update(related_fields)
return fields
def _get_assets(self, resource, value):
endpoint = getattr(self, resource)
if value:
from awxkit.cli.options import pk_or_name
pk = pk_or_name(self, resource, value) # TODO: decide whether to support multiple
results = endpoint.get(id=pk).results
else:
results = endpoint.get(all_pages=True).results
options = self._get_options(endpoint)
assets = (self._serialize_asset(asset, options) for asset in results)
return [asset for asset in assets if asset is not None]
def export_assets(self, **kwargs):
# If no resource kwargs are explicitly used, export everything.
all_resources = all(kwargs.get(resource) is None for resource in EXPORTABLE_RESOURCES)
data = {}
for resource in EXPORTABLE_RESOURCES:
value = kwargs.get(resource)
if all_resources or value is not None:
data[resource] = self._get_assets(resource, value)
return data
# Import methods
def _dependent_resources(self, data):
page_resource = {getattr(self, resource)._create().__item_class__: resource
for resource in self.json}
data_pages = [getattr(self, resource)._create().__item_class__ for resource in data]
for page_cls in itertools.chain(*has_create.page_creation_order(*data_pages)):
yield page_resource[page_cls]
def _register_page(self, page):
natural_key = freeze(get_natural_key(page))
# FIXME: we need to keep a reference for the case where we
# don't have a natural key, so we can delete
if natural_key is not None:
if getattr(self, '_natural_key', None) is None:
self._natural_key = {}
self._natural_key[natural_key] = page
def _register_existing_assets(self, resource):
endpoint = getattr(self, resource)
options = self._get_options(endpoint)
if getattr(self, '_options', None) is None:
self._options = {}
self._options[resource] = options
results = endpoint.get(all_pages=True).results
for pg in results:
self._register_page(pg)
def _get_by_natural_key(self, key, fetch=True):
frozen_key = freeze(key)
if frozen_key is not None and frozen_key not in self._natural_key and fetch:
pass # FIXME
return self._natural_key.get(frozen_key)
def _create_assets(self, data, resource):
if resource not in data or resource not in EXPORTABLE_RESOURCES:
return
endpoint = getattr(self, resource)
options = self._options[resource]
assets = data[resource]
for asset in assets:
post_data = {}
for field, value in asset.items():
if field not in options:
continue
if options[field]['type'] == 'id':
page = self._get_by_natural_key(value)
post_data[field] = page['id'] if page is not None else None
else:
post_data[field] = value
page = self._get_by_natural_key(asset['natural_key'], fetch=False)
if page is None:
if resource == 'users':
# We should only impose a default password if the resource doesn't exist.
post_data.setdefault('password', 'abc123')
page = endpoint.post(post_data)
else:
page = page.put(post_data)
self._register_page(page)
def _assign_roles(self, page, roles):
role_endpoint = page.json['related']['roles']
for role in roles:
if 'content_object' not in role:
continue # admin role
obj_page = self._get_by_natural_key(role['content_object'])
if obj_page is not None:
role_page = obj_page.get_object_role(role['name'], by_name=True)
try:
role_endpoint.post({'id': role_page['id']})
except exc.NoContent: # desired exception on successful (dis)association
pass
else:
pass # admin role
def _assign_related(self, page, name, related_set):
pass # FIXME
def _assign_related_assets(self, resource, assets):
for asset in assets:
page = self._get_by_natural_key(asset['natural_key'])
# FIXME: deal with `page is None` case
for name, S in asset.get('related', {}).items():
if name == 'roles':
self._assign_roles(page, S)
else:
self._assign_related(page, name, S)
def import_assets(self, data):
for resource in self._dependent_resources(data):
self._register_existing_assets(resource)
self._create_assets(data, resource)
# FIXME: should we delete existing unpatched assets?
for resource, assets in data.items():
self._assign_related_assets(resource, assets)
page.register_page(resources.v2, ApiV2)