/usr/lib/python3/dist-packages/maascli/actions/boot_resources_create.py is in python3-maas-client 2.4.0~beta2-6865-gec43e47e6-0ubuntu1.
This file is owned by root:root, with mode 0o644.
The actual contents of the file can be viewed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | # Copyright 2014-2016 Canonical Ltd. This software is licensed under the
# GNU Affero General Public License version 3 (see the file LICENSE).
"""MAAS Boot Resources Action."""
__all__ = [
'action_class'
]
import hashlib
from io import BytesIO
import json
from urllib.parse import urljoin
from apiclient.multipart import (
build_multipart_message,
encode_multipart_message,
)
from maascli import utils
from maascli.api import (
Action,
http_request,
)
from maascli.command import CommandError
# Send 4MB of data per request.
CHUNK_SIZE = 1 << 22
class BootResourcesCreateAction(Action):
"""Provides custom logic to the boot-resources create action.
Command: maas username boot-resources create
The create command has the ability to upload the content in pieces, using
the upload_uri that is returned in the response. This class provides the
logic to upload over that API.
"""
def __call__(self, options):
# TODO: this is el-cheapo URI Template
# <http://tools.ietf.org/html/rfc6570> support; use uritemplate-py
# <https://github.com/uri-templates/uritemplate-py> here?
uri = self.uri.format(**vars(options))
content = self.initial_request(uri, options)
# Get the created resource file for the boot resource
rfile = self.get_resource_file(content)
if rfile is None:
print("Failed to identify created resource.")
raise CommandError(2)
if rfile['complete']:
# File already existed in the database, so no
# reason to upload it.
return
# Upload content
data = dict(options.data)
upload_uri = urljoin(uri, rfile['upload_uri'])
self.upload_content(
upload_uri, data['content'], insecure=options.insecure)
def initial_request(self, uri, options):
"""Performs the initial POST request, to create the boot resource."""
# Bundle things up ready to throw over the wire.
body, headers = self.prepare_initial_payload(options.data)
# Headers are returned as a list, but they must be a dict for
# the signing machinery.
headers = dict(headers)
# Sign request if credentials have been provided.
if self.credentials is not None:
self.sign(uri, headers, self.credentials)
# Use httplib2 instead of urllib2 (or MAASDispatcher, which is based
# on urllib2) so that we get full control over HTTP method. TODO:
# create custom MAASDispatcher to use httplib2 so that MAASClient can
# be used.
response, content = http_request(
uri, self.method, body=body, headers=headers,
insecure=options.insecure)
# 2xx status codes are all okay.
if response.status // 100 != 2:
if options.debug:
utils.dump_response_summary(response)
utils.print_response_content(response, content)
raise CommandError(2)
return content
def prepare_initial_payload(self, data):
"""Return the body and headers for the initial request.
This is method is only used for the first request to MAAS. It
removes the passed content, and replaces it with the sha256 and size
of that content.
:param data: An iterable of ``name, value`` or ``name, opener``
tuples (see `name_value_pair`) to pack into the body or
query, depending on the type of request.
"""
data = dict(data)
if 'content' not in data:
print('Missing content.')
raise CommandError(2)
content = data.pop('content')
size, sha256 = self.calculate_size_and_sha256(content)
data['size'] = '%s' % size
data['sha256'] = sha256
data = sorted((key, value) for key, value in data.items())
message = build_multipart_message(data)
headers, body = encode_multipart_message(message)
return body, headers
def calculate_size_and_sha256(self, content):
"""Return the size and sha256 of the content."""
size = 0
sha256 = hashlib.sha256()
with content() as fd:
while True:
buf = fd.read(CHUNK_SIZE)
length = len(buf)
size += length
sha256.update(buf)
if length != CHUNK_SIZE:
break
return size, sha256.hexdigest()
def get_resource_file(self, content):
"""Return the boot resource file for the created file."""
if isinstance(content, bytes):
content = content.decode('utf-8')
data = json.loads(content)
if len(data['sets']) == 0:
# No sets returned, no way to get the resource file.
return None
newest_set = sorted(data['sets'].keys(), reverse=True)[0]
resource_set = data['sets'][newest_set]
if len(resource_set['files']) != 1:
# This api only supports uploading one file. If the set doesn't
# have just one file, then there is no way of knowing which file.
return None
_, rfile = resource_set['files'].popitem()
return rfile
def put_upload(self, upload_uri, data, insecure=False):
"""Send PUT method to upload data."""
headers = {
'Content-Type': 'application/octet-stream',
'Content-Length': '%s' % len(data),
}
if self.credentials is not None:
self.sign(upload_uri, headers, self.credentials)
# httplib2 expects the body to be file-like if its binary data
data = BytesIO(data)
response, content = http_request(
upload_uri, 'PUT', body=data, headers=headers,
insecure=insecure)
if response.status != 200:
utils.print_response_content(response, content)
raise CommandError(2)
def upload_content(self, upload_uri, content, insecure=False):
"""Upload the content in chunks."""
with content() as fd:
while True:
buf = fd.read(CHUNK_SIZE)
length = len(buf)
if length > 0:
self.put_upload(upload_uri, buf, insecure=insecure)
if length != CHUNK_SIZE:
break
# Each action sets this variable so the class can be picked up
# by get_action_class.
action_class = BootResourcesCreateAction
|