# Copyright 2017 AT&T Intellectual Property. All other rights reserved. # # 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. """Handle resources for boot action API endpoints. """ import falcon import ulid2 import tarfile import io import logging from .base import StatefulResource logger = logging.getLogger('drydock') class BootactionResource(StatefulResource): def __init__(self, orchestrator=None, **kwargs): super().__init__(**kwargs) self.orchestrator = orchestrator def on_post(self, req, resp, action_id): """Post status messages or final status for a boot action. This endpoint does not use the standard oslo_policy enforcement as this endpoint is accessed by unmanned nodes. Instead it uses a internal key authentication :param req: falcon request :param resp: falcone response :param action_id: ULID ID of the boot action """ class BootactionAssetsResource(StatefulResource): def __init__(self, orchestrator=None, **kwargs): super().__init__(**kwargs) self.orchestrator = orchestrator def do_get(self, req, resp, hostname, asset_type): """Render ``unit`` type boot action assets for hostname. Get the boot action context for ``hostname`` from the database and render all ``unit`` type assets for the host. Validate host is providing the correct idenity key in the ``X-Bootaction-Key`` header. :param req: falcon request object :param resp: falcon response object :param hostname: URL path parameter indicating the calling host :param asset_type: Asset type to include in the response - ``unit``, ``file``, ``pkg_list``, ``all`` """ try: ba_ctx = self.state_manager.get_boot_action_context(hostname) except Exception as ex: self.logger.error( "Error locating boot action for %s" % hostname, exc_info=ex) raise falcon.HTTPNotFound() if ba_ctx is None: raise falcon.HTTPNotFound( description="Error locating boot action for %s" % hostname) BootactionUtils.check_auth(ba_ctx, req) asset_type_filter = None if asset_type == 'all' else asset_type try: task = self.state_manager.get_task(ba_ctx['task_id']) design_status, site_design = self.orchestrator.get_effective_site( task.design_ref) assets = list() for ba in site_design.bootactions: if hostname in ba.target_nodes: action_id = ulid2.generate_binary_ulid() assets.extend( ba.render_assets( hostname, site_design, action_id, type_filter=asset_type_filter)) self.state_manager.post_boot_action( hostname, ba_ctx['task_id'], ba_ctx['identity_key'], action_id) tarball = BootactionUtils.tarbuilder(asset_list=assets) resp.set_header('Content-Type', 'application/gzip') resp.set_header('Content-Disposition', "attachment; filename=\"%s-%s.tar.gz\"" % (hostname, asset_type)) resp.data = tarball resp.status = falcon.HTTP_200 return except Exception as ex: self.logger.debug("Exception in boot action API.", exc_info=ex) raise falcon.HTTPInternalServerError(str(ex)) class BootactionUnitsResource(BootactionAssetsResource): def __init__(self, **kwargs): super().__init__(**kwargs) def on_get(self, req, resp, hostname): self.logger.debug( "Accessing boot action units resource for host %s." % hostname) super().do_get(req, resp, hostname, 'unit') class BootactionFilesResource(BootactionAssetsResource): def __init__(self, **kwargs): super().__init__(**kwargs) def on_get(self, req, resp, hostname): super().do_get(req, resp, hostname, 'file') class BootactionUtils(object): """Utility class shared by Boot Action API resources.""" @staticmethod def check_auth(ba_ctx, req): """Check request authentication based on boot action context. Raise proper Falcon exception if authentication fails, otherwise silently return :param ba_ctx: Boot Action context from database :param req: The falcon request object of the API call """ identity_key = req.get_header('X-Bootaction-Key', default='') if identity_key == '': raise falcon.HTTPUnauthorized( title='Unauthorized', description='No X-Bootaction-Key', challenges=['Bootaction-Key']) if ba_ctx['identity_key'] != bytes.fromhex(identity_key): logger.warn( "Forbidding boot action access - node: %s, identity_key: %s, req header: %s" % (ba_ctx['node_name'], str(ba_ctx['identity_key']), str(bytes.fromhex(identity_key)))) raise falcon.HTTPForbidden( title='Unauthorized', description='Invalid X-Bootaction-Key') @staticmethod def tarbuilder(asset_list=None): """Create a tar file from rendered assets. Add each asset in ``asset_list`` to a tar file with the defined path and permission. The assets need to have the rendered_bytes field populated. Return a tarfile.TarFile. :param hostname: the hostname the tar is destined for :param balltype: the type of assets being included :param asset_list: list of objects.BootActionAsset instances """ tarbytes = io.BytesIO() tarball = tarfile.open( mode='w:gz', fileobj=tarbytes, format=tarfile.GNU_FORMAT) asset_list = asset_list or [] for a in asset_list: fileobj = io.BytesIO(a.rendered_bytes) tarasset = tarfile.TarInfo(name=a.path) tarasset.size = len(a.rendered_bytes) tarasset.mode = a.permissions if a.permissions else 0o600 tarasset.uid = 0 tarasset.gid = 0 tarball.addfile(tarasset, fileobj=fileobj) tarball.close() return tarbytes.getvalue()