Merge lp:~cjohnston/ubuntu-ci-services-itself/get-swift-image into lp:ubuntu-ci-services-itself
- get-swift-image
- Merge into trunk
| Status: | Merged |
|---|---|
| Approved by: | Chris Johnston |
| Approved revision: | 405 |
| Merged at revision: | 410 |
| Proposed branch: | lp:~cjohnston/ubuntu-ci-services-itself/get-swift-image |
| Merge into: | lp:ubuntu-ci-services-itself |
| Diff against target: | 442 lines (+300/-25) 6 files modified ci-utils/ci_utils/data_store/__init__.py (+2/-2) cli/ci_libs/image.py (+93/-0) cli/ci_libs/ticket.py (+2/-1) cli/ci_libs/utils.py (+6/-1) cli/tests/test_image.py (+168/-0) cli/ubuntu-ci (+29/-21) |
| To merge this branch: | bzr merge lp:~cjohnston/ubuntu-ci-services-itself/get-swift-image |
| Related bugs: |
| Reviewer | Review Type | Date Requested | Status |
|---|---|---|---|
| PS Jenkins bot (community) | continuous-integration | Approve | |
| Chris Johnston (community) | Needs Resubmitting | ||
| Andy Doan (community) | Approve | ||
| Review via email: | |||
Commit message
Add the ability to get_image to the CLI
Description of the change
The CI Engine builds a release candidate image that includes all of the tested and 'approved' packages included in it.
Currently the built images are stored in glance and swift. The images stored in glance are not always available for users to download due to different provider policies. Because of this we had to start storing the images in swift. They are presently stored in a container called ticket-#-image (where # is the ticket number). This container is a private container to avoid the images being available without credentials (NOTE: we should probably make the public/private availability a choice either at deployment time or at ticket creation time).
Because the image containers are presently set to be private, even though the artifact reference can be seen on the WebUI, it isn't possible for a user to download the image via the WebUI since they are missing the proper credentials. Because of this, we needed a way for the user to be able to download the image with the proper credentials.
Also note that Ursula is working on some additional tests for this MP which will follow soon.
| PS Jenkins bot (ps-jenkins) wrote : | # |
| Ursula Junque (ursinha) wrote : | # |
+class GetTicketImage(
I changed this in my branch earlier today and I think it got lost in the tons of changes after that: these tests don't check anything gnupg related, so I think we can use unittest.TestCase here.
- 402. By Chris Johnston
-
Update per review - use unittest
> Because the image containers are presently set to be private, even though the
> artifact reference can be seen on the WebUI, it isn't possible for a user to
> download the image via the WebUI since they are missing the proper
> credentials. Because of this, we needed a way for the user to be able to
> download the image with the proper credentials.
Just to throw support behind this, I think it's entirely reasonable that we do not provide access to the images via the webui in phase 0 (and I think we discussed as much on IRC when planning where this feature would get implemented).
Once we're past phase 0, I want to see us put data store object creation behind a new intermediary service that manages the credentials. The CLI should just send the signed GPG content and this intermediary should validate the signature. No cloud credentials should change hands.
This is bug 1288710.
| Andy Doan (doanac) wrote : | # |
On 03/16/2014 06:33 PM, Chris Johnston wrote:
> +def get_image(args):
> + if args.ticket:
> + ticket = args.ticket
> + else:
> + ticket_status_base = utils.CI_URL + utils.TICKET_
> + url = ticket_status_base + '?current_
> + ticket_
> + data = utils.get(url)
> + tickets = []
> + for o in data['objects']:
> + tickets.
> + tickets = sorted(tickets, reverse=True)
> + ticket = tickets.pop(0)
You could do this in one line with:
ticket = max([x['id'] for x in data['objects']])
If there are no completed tickets, does data['objects'] exist? Maybe to
make this super safe you do:
ticket = max([x['id'] for x in data.get('objects', [])])
> +
> + data = _get_image_
> + ticket_id = str(ticket) + '-image'
> + ds = data_store.
> + auth_config=
> + public=False)
> + image = data['objects'
> + local_path = os.path.
I'm not a fan of this fixed download location. I'd want to specify via
the CLI and not have this throwing stuff under my $HOME. This might just
be me.
> + if os.path.
> + print("{} already exists.
> + else:
> + try:
> + download = ds.get_file(image)
> + fp = open(local_path, 'wb')
> + fp.write(download)
Is there a way we can have swift stream this to the file descriptor
rather than loading the ~200M into memory and writing it all to disk?
| Francis Ginther (fginther) wrote : | # |
The mechanics of this look good and I was able to test that it works (from a ticket I submitted earlier today). I would prefer that it drop the image into my current dir or support an option to specify the location.
If there are significant issues with addressing Andy's comments, then I would strongly consider approving this as it does fill a functionality gap.
| Joe Talbott (joetalbott) wrote : | # |
L60 I think the preferred syntax is; except Blah as b: rather than; except Blah, b:
L235 I prefer to name my test classes as TextXYZ so they stand out visually to me as tests.
I agree with @Andy regarding the download location and avoiding reading the entire file into memory.
| Joe Talbott (joetalbott) wrote : | # |
Hrm, after a bit of research, swiftclient.
- 403. By Chris Johnston
-
merge trunk
- 404. By Chris Johnston
-
Change the way to get the newest ticket per review
| Andy Doan (doanac) wrote : | # |
lets get this merged. and then fix the saving logic separately but soon.
- 405. By Chris Johnston
-
Require a user to specify where they would like to download their RC image to
| Chris Johnston (cjohnston) wrote : | # |
r404 changes how we get the 'latest' completed ticket
r405 changes to require the user to specify where they want to download their image to
| PS Jenkins bot (ps-jenkins) wrote : | # |
PASSED: Continuous integration, rev:405
http://
Executed test runs:
Click here to trigger a rebuild:
http://
| Andy Doan (doanac) wrote : | # |
looks good. we should just get this merged i think. one note:
246 + self.image_path = os.path.join(
247 + utils.HOME, 'd62d3d9a-
248 + self.addCleanup
you could use tempfile for that now so the test doesn't mess with $HOME
Preview Diff
| 1 | === modified file 'ci-utils/ci_utils/data_store/__init__.py' |
| 2 | --- ci-utils/ci_utils/data_store/__init__.py 2014-03-10 22:25:00 +0000 |
| 3 | +++ ci-utils/ci_utils/data_store/__init__.py 2014-03-17 17:17:40 +0000 |
| 4 | @@ -23,8 +23,8 @@ |
| 5 | pass |
| 6 | |
| 7 | |
| 8 | -def create_for_ticket(ticket_id, auth_config): |
| 9 | - return DataStore('ticket-{}'.format(ticket_id), auth_config, public=True) |
| 10 | +def create_for_ticket(ticket_id, auth_config, public=True): |
| 11 | + return DataStore('ticket-{}'.format(ticket_id), auth_config, public=public) |
| 12 | |
| 13 | |
| 14 | def _get_file_name(filename): |
| 15 | |
| 16 | === added file 'cli/ci_libs/image.py' |
| 17 | --- cli/ci_libs/image.py 1970-01-01 00:00:00 +0000 |
| 18 | +++ cli/ci_libs/image.py 2014-03-17 17:17:40 +0000 |
| 19 | @@ -0,0 +1,93 @@ |
| 20 | +# Ubuntu Continuous Integration Engine |
| 21 | +# Copyright 2014 Canonical Ltd. |
| 22 | +# |
| 23 | +# This program is free software: you can redistribute it and/or modify it under |
| 24 | +# the terms of the GNU General Public License version 3, as published by the |
| 25 | +# Free Software Foundation. |
| 26 | +# |
| 27 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
| 28 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
| 29 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 30 | +# General Public License for more details. |
| 31 | +# |
| 32 | +# You should have received a copy of the GNU General Public License along |
| 33 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
| 34 | + |
| 35 | +import os |
| 36 | +import sys |
| 37 | +import urllib2 |
| 38 | + |
| 39 | +from ci_utils import data_store, ticket_states |
| 40 | +from ci_libs import utils |
| 41 | + |
| 42 | + |
| 43 | +class ImageObjectNotFound(Exception): |
| 44 | + """Raised when image object cannot be found.""" |
| 45 | + def __init__(self, message): |
| 46 | + self.filename = message.split(",")[0].split(": ")[1] |
| 47 | + self.url = message.split("Error: Object GET failed: ")[1].split( |
| 48 | + " 404 Not Found")[0] |
| 49 | + |
| 50 | + def __str__(self): |
| 51 | + return "404 Not Found: {}".format(self.url) |
| 52 | + |
| 53 | + |
| 54 | +def _get_image_artifact(ticket): |
| 55 | + artifact_base = utils.CI_URL + utils.TICKET_ARTIFACT_BASE |
| 56 | + try: |
| 57 | + url = artifact_base + '?type__exact=IMAGE&ticket__exact={}'.format( |
| 58 | + ticket) |
| 59 | + return utils.get(url) |
| 60 | + except urllib2.HTTPError, exc: |
| 61 | + if exc.code == 404 and ticket: |
| 62 | + sys.exit("Ticket number {} not found.".format(ticket)) |
| 63 | + raise |
| 64 | + except urllib2.URLError, exc: |
| 65 | + raise |
| 66 | + |
| 67 | + |
| 68 | +def _download(ticket, name, args_ticket): |
| 69 | + data = _get_image_artifact(ticket) |
| 70 | + ticket_id = str(ticket) + '-image' |
| 71 | + ds = data_store.create_for_ticket(ticket_id=ticket_id, |
| 72 | + auth_config=utils.AUTH_CONFIG, |
| 73 | + public=False) |
| 74 | + image = data['objects'][0][u'name'] |
| 75 | + try: |
| 76 | + download = ds.get_file(image) |
| 77 | + fp = open(name, 'wb') |
| 78 | + fp.write(download) |
| 79 | + if args_ticket: |
| 80 | + print("Successfully downloaded image for ticket #{}.\n" |
| 81 | + "Image file can be found at: {}".format(ticket, name)) |
| 82 | + else: |
| 83 | + print("Successfully downloaded latest release candidate image.\n" |
| 84 | + "Image file can be found at: {}".format(name)) |
| 85 | + except data_store.DataStoreException as exc: |
| 86 | + if "404 Not Found" in exc.args[0]: |
| 87 | + raise ImageObjectNotFound(exc.message) |
| 88 | + raise |
| 89 | + except IOError: |
| 90 | + raise |
| 91 | + |
| 92 | + |
| 93 | +def get_image(args): |
| 94 | + if args.ticket: |
| 95 | + ticket = args.ticket |
| 96 | + else: |
| 97 | + ticket_status_base = utils.CI_URL + utils.TICKET_STATUS_BASE |
| 98 | + url = ticket_status_base + '?current_workflow_step={}'.format( |
| 99 | + ticket_states.TicketWorkflowStep.COMPLETED.value) |
| 100 | + data = utils.get(url) |
| 101 | + try: |
| 102 | + ticket = max([x['id'] for x in data.get('objects', [])]) |
| 103 | + except ValueError: |
| 104 | + sys.exit("No completed tickets found.") |
| 105 | + |
| 106 | + if os.path.exists(args.name): |
| 107 | + print("{} already exists.".format(args.name)) |
| 108 | + elif not os.path.isdir(os.path.dirname(args.name)): |
| 109 | + os.makedirs(os.path.dirname(args.name)) |
| 110 | + _download(ticket, args.name, args.ticket) |
| 111 | + else: |
| 112 | + _download(ticket, args.name, args.ticket) |
| 113 | |
| 114 | === modified file 'cli/ci_libs/ticket.py' |
| 115 | --- cli/ci_libs/ticket.py 2014-01-29 16:51:27 +0000 |
| 116 | +++ cli/ci_libs/ticket.py 2014-03-17 17:17:40 +0000 |
| 117 | @@ -98,7 +98,8 @@ |
| 118 | "name": file, |
| 119 | "subticket": self.subticket_uri, |
| 120 | } |
| 121 | - location_ = utils.post(utils.CI_URL + utils.ARTIFACT_BASE, data=data) |
| 122 | + location_ = utils.post(utils.CI_URL + utils.SUBTICKET_ARTIFACT_BASE, |
| 123 | + data=data) |
| 124 | log.info("Created artifact: %s" % location_) |
| 125 | |
| 126 | |
| 127 | |
| 128 | === modified file 'cli/ci_libs/utils.py' |
| 129 | --- cli/ci_libs/utils.py 2014-03-10 22:25:00 +0000 |
| 130 | +++ cli/ci_libs/utils.py 2014-03-17 17:17:40 +0000 |
| 131 | @@ -13,6 +13,7 @@ |
| 132 | # You should have received a copy of the GNU General Public License along |
| 133 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
| 134 | |
| 135 | +import os |
| 136 | import json |
| 137 | import logging |
| 138 | import sys |
| 139 | @@ -29,8 +30,12 @@ |
| 140 | TICKET_BASE = API_URL + 'ticket/' |
| 141 | SPU_BASE = API_URL + 'spu/' |
| 142 | SOURCEPACKAGE_BASE = API_URL + 'sourcepackage/' |
| 143 | -ARTIFACT_BASE = API_URL + 'subticketartifact/' |
| 144 | +TICKET_ARTIFACT_BASE = API_URL + 'ticketartifact/' |
| 145 | +SUBTICKET_ARTIFACT_BASE = API_URL + 'subticketartifact/' |
| 146 | SUBTICKET_BASE = API_URL + 'subticket/' |
| 147 | +TICKET_STATUS_BASE = API_URL + 'ticketstatus/' |
| 148 | +HOME = os.environ["HOME"] |
| 149 | +DEF_CFG = os.path.join(HOME, '.ubuntu-ci') |
| 150 | |
| 151 | CI_URL = None |
| 152 | AUTH_CONFIG = None |
| 153 | |
| 154 | === added file 'cli/tests/test_image.py' |
| 155 | --- cli/tests/test_image.py 1970-01-01 00:00:00 +0000 |
| 156 | +++ cli/tests/test_image.py 2014-03-17 17:17:40 +0000 |
| 157 | @@ -0,0 +1,168 @@ |
| 158 | +#!/usr/bin/env python |
| 159 | +# -*- coding: utf-8 -*- |
| 160 | +# Ubuntu Continuous Integration Engine |
| 161 | +# Copyright 2014 Canonical Ltd. |
| 162 | +# |
| 163 | +# This program is free software: you can redistribute it and/or modify it under |
| 164 | +# the terms of the GNU General Public License version 3, as published by the |
| 165 | +# Free Software Foundation. |
| 166 | +# |
| 167 | +# This program is distributed in the hope that it will be useful, but WITHOUT |
| 168 | +# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY, |
| 169 | +# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 170 | +# General Public License for more details. |
| 171 | +# |
| 172 | +# You should have received a copy of the GNU General Public License along |
| 173 | +# with this program. If not, see <http://www.gnu.org/licenses/>. |
| 174 | + |
| 175 | +import imp |
| 176 | +import json |
| 177 | +import mock |
| 178 | +import os |
| 179 | +import unittest |
| 180 | + |
| 181 | +from ci_utils import data_store |
| 182 | + |
| 183 | +from ci_libs import utils |
| 184 | +from ci_libs.image import ImageObjectNotFound |
| 185 | +from tests import capture_stdout |
| 186 | + |
| 187 | +import urllib2 |
| 188 | +urllib2.urlopen = mock.Mock() |
| 189 | + |
| 190 | + |
| 191 | +class FakeUrlOpenReturn: |
| 192 | + """Object to mock the return of urllib2.urlopen.""" |
| 193 | + |
| 194 | + def __init__(self, code, content): |
| 195 | + self.code = code |
| 196 | + self.content = content |
| 197 | + |
| 198 | + def __repr__(self): |
| 199 | + return self.content |
| 200 | + |
| 201 | + def __iter__(self): |
| 202 | + return (line for line in self.content.split("\n")) |
| 203 | + |
| 204 | + def read(self): |
| 205 | + return self.content |
| 206 | + |
| 207 | + |
| 208 | +artifact_data = { |
| 209 | + "objects": [ |
| 210 | + { |
| 211 | + "id": 6, |
| 212 | + "name": "d62d3d9a-ac3d-11e3-847d-fa163eaf5928.img", |
| 213 | + "reference": "http://www.example.com", |
| 214 | + "resource_uri": "/api/v1/ticketartifact/6/", |
| 215 | + "ticket": { |
| 216 | + "id": 4, |
| 217 | + }, |
| 218 | + "type": "IMAGE" |
| 219 | + }, |
| 220 | + ] |
| 221 | +} |
| 222 | + |
| 223 | +ticket_data = { |
| 224 | + "objects": [ |
| 225 | + { |
| 226 | + "id": 1, |
| 227 | + }, |
| 228 | + { |
| 229 | + "id": 4, |
| 230 | + }, |
| 231 | + { |
| 232 | + "id": 2, |
| 233 | + }, |
| 234 | + ], |
| 235 | +} |
| 236 | + |
| 237 | + |
| 238 | +class TestGetTicketImage(unittest.TestCase): |
| 239 | + |
| 240 | + def setUp(self): |
| 241 | + super(TestGetTicketImage, self).setUp() |
| 242 | + self.cli = imp.load_source( |
| 243 | + "ubuntu-ci", |
| 244 | + os.path.join(os.path.dirname(__file__), "../ubuntu-ci") |
| 245 | + ) |
| 246 | + self.image_path = os.path.join( |
| 247 | + utils.HOME, 'd62d3d9a-ac3d-11e3-847d-fa163eaf5928.img') |
| 248 | + self.addCleanup(self._remove_image) |
| 249 | + |
| 250 | + def _remove_image(self): |
| 251 | + if os.path.exists(self.image_path): |
| 252 | + os.remove(self.image_path) |
| 253 | + |
| 254 | + @mock.patch('ci_utils.data_store.create_for_ticket') |
| 255 | + def test_get_ticket_image(self, mock_create_datastore): |
| 256 | + urllib2.urlopen.side_effect = [ |
| 257 | + FakeUrlOpenReturn(200, json.dumps(artifact_data))] |
| 258 | + mock_datastore = mock.Mock() |
| 259 | + mock_datastore.get_file.return_value = buffer(" ") |
| 260 | + mock_create_datastore.return_value = mock_datastore |
| 261 | + |
| 262 | + args = self.cli.parse_arguments(['get_image', '-t', '4', '-n', |
| 263 | + self.image_path]) |
| 264 | + with capture_stdout(args.func, args) as cm: |
| 265 | + self.assertEqual( |
| 266 | + cm, |
| 267 | + "Successfully downloaded image for ticket #4.\n" |
| 268 | + "Image file can be found at: {}\n".format(self.image_path)) |
| 269 | + mock_datastore.assert_called_once() |
| 270 | + |
| 271 | + @mock.patch('ci_utils.data_store.create_for_ticket') |
| 272 | + def test_get_ticket_image_not_found(self, mock_create_datastore): |
| 273 | + urllib2.urlopen.side_effect = [ |
| 274 | + FakeUrlOpenReturn(200, json.dumps(artifact_data))] |
| 275 | + mock_datastore = mock.Mock() |
| 276 | + mock_datastore.get_file.side_effect = data_store.DataStoreException( |
| 277 | + "Failed to get file: d62d3d9a-ac3d-11e3-847d-fa163eaf5928.img, " |
| 278 | + " Error: Object GET failed: https://swift.canonistack.canonical." |
| 279 | + "com:443/v1/AUTH_2de6e093b42c44b58a09758c347535ff/ticket-1-image/" |
| 280 | + "d62d3d9a-ac3d-11e3-847d-fa163eaf5928.img 404 Not Found") |
| 281 | + mock_create_datastore.return_value = mock_datastore |
| 282 | + |
| 283 | + args = self.cli.parse_arguments(['get_image', '-t', '4', '-n', |
| 284 | + self.image_path]) |
| 285 | + with self.assertRaises(ImageObjectNotFound): |
| 286 | + args.func(args) |
| 287 | + |
| 288 | + @mock.patch('ci_utils.data_store.DataStore') |
| 289 | + def test_get_ticket_image_already_exists(self, mock_data_store): |
| 290 | + urllib2.urlopen.side_effect = [ |
| 291 | + FakeUrlOpenReturn(200, json.dumps(artifact_data))] |
| 292 | + open(self.image_path, 'a').close() |
| 293 | + args = self.cli.parse_arguments(['get_image', '-t', '4', '-n', |
| 294 | + self.image_path]) |
| 295 | + with capture_stdout(args.func, args) as cm: |
| 296 | + self.assertEqual( |
| 297 | + "{} already exists.\n".format(self.image_path), |
| 298 | + cm) |
| 299 | + |
| 300 | + @mock.patch('ci_utils.data_store.create_for_ticket') |
| 301 | + def test_get_last_completed_ticket_image(self, mock_create_datastore): |
| 302 | + urllib2.urlopen.side_effect = [ |
| 303 | + FakeUrlOpenReturn(200, json.dumps(ticket_data)), |
| 304 | + FakeUrlOpenReturn(200, json.dumps(artifact_data)) |
| 305 | + ] |
| 306 | + mock_datastore = mock.Mock() |
| 307 | + mock_datastore.get_file.return_value = buffer(" ") |
| 308 | + mock_create_datastore.return_value = mock_datastore |
| 309 | + |
| 310 | + args = self.cli.parse_arguments(['get_image', '-n', self.image_path]) |
| 311 | + with capture_stdout(args.func, args) as cm: |
| 312 | + self.assertEqual( |
| 313 | + cm, |
| 314 | + "Successfully downloaded latest release candidate image.\n" |
| 315 | + "Image file can be found at: {}\n".format(self.image_path)) |
| 316 | + mock_datastore.assert_called_once() |
| 317 | + |
| 318 | + def test_get_last_completed_ticket_none_complete(self): |
| 319 | + data = {'objects': []} |
| 320 | + urllib2.urlopen.side_effect = [ |
| 321 | + FakeUrlOpenReturn(200, json.dumps(data))] |
| 322 | + args = self.cli.parse_arguments(['get_image', '-n', self.image_path]) |
| 323 | + with self.assertRaises(SystemExit) as cm: |
| 324 | + args.func(args) |
| 325 | + self.assertEqual("No completed tickets found.", cm.exception.message) |
| 326 | |
| 327 | === modified file 'cli/ubuntu-ci' |
| 328 | --- cli/ubuntu-ci 2014-03-10 22:25:00 +0000 |
| 329 | +++ cli/ubuntu-ci 2014-03-17 17:17:40 +0000 |
| 330 | @@ -22,19 +22,13 @@ |
| 331 | import urllib2 |
| 332 | |
| 333 | from ci_libs import ( |
| 334 | + ticket, |
| 335 | file_handler, |
| 336 | status, |
| 337 | utils, |
| 338 | + image, |
| 339 | ) |
| 340 | -from ci_libs.file_handler import (ChangesFileNotFound, ChangesFileException, |
| 341 | - ChangesParseError, ChangesValidationError, |
| 342 | - FileToUploadNotFound, NotAChangesFileError, |
| 343 | - UploadDirNotFound) |
| 344 | -from ci_libs.ticket import new_ticket |
| 345 | -from ci_utils.data_store import DataStoreException |
| 346 | -from ci_utils import dump_stack |
| 347 | - |
| 348 | -DEF_CFG = os.path.join(os.environ["HOME"], '.ubuntu-ci') |
| 349 | +from ci_utils import dump_stack, data_store |
| 350 | |
| 351 | |
| 352 | def parse_arguments(args=None): |
| 353 | @@ -65,7 +59,7 @@ |
| 354 | 'in the same directory as their respective ' |
| 355 | 'source.changes.', required=True) |
| 356 | |
| 357 | - ticket_parser.set_defaults(func=new_ticket) |
| 358 | + ticket_parser.set_defaults(func=ticket.new_ticket) |
| 359 | status_parser = subparsers.add_parser('status', |
| 360 | help='Get ticket status. Use no ' |
| 361 | 'flags for all tickets') |
| 362 | @@ -73,6 +67,16 @@ |
| 363 | help='Ticket to display status of. Leave off ' |
| 364 | 'for all tickets') |
| 365 | status_parser.set_defaults(func=status.ticket_status) |
| 366 | + image_parser = subparsers.add_parser('get_image', |
| 367 | + help='Retrieve the image produced by ' |
| 368 | + 'a ticket.') |
| 369 | + image_parser.add_argument('-t', '--ticket', |
| 370 | + help='Ticket to display status of. Leave off ' |
| 371 | + 'for last successful ticket') |
| 372 | + image_parser.add_argument('-n', '--name', |
| 373 | + help='Desired file name (and path) for the ' |
| 374 | + 'downloaded image', required=True) |
| 375 | + image_parser.set_defaults(func=image.get_image) |
| 376 | return parser.parse_args(args) |
| 377 | |
| 378 | |
| 379 | @@ -105,9 +109,9 @@ |
| 380 | # files are found. |
| 381 | for source in args.sources: |
| 382 | if not os.path.exists(source): |
| 383 | - raise ChangesFileNotFound(source) |
| 384 | + raise file_handler.ChangesFileNotFound(source) |
| 385 | if not source.endswith(".changes"): |
| 386 | - raise NotAChangesFileError(source) |
| 387 | + raise file_handler.NotAChangesFileError(source) |
| 388 | # Validating -a option is properly formatted. |
| 389 | utils.assert_valid_package_list(args.add) |
| 390 | # Validating -r option is properly formatted. |
| 391 | @@ -117,29 +121,33 @@ |
| 392 | # We're not in create_ticket context, moving on. |
| 393 | pass |
| 394 | |
| 395 | - utils.load_config(DEF_CFG) |
| 396 | + utils.load_config(utils.DEF_CFG) |
| 397 | args.func(args) |
| 398 | return 0 |
| 399 | except utils.InputError as exc: |
| 400 | log.error(exc) |
| 401 | - except ChangesFileNotFound as exc: |
| 402 | + except file_handler.ChangesFileNotFound as exc: |
| 403 | log.error("Changes file not found: {}".format(exc)) |
| 404 | - except NotAChangesFileError as exc: |
| 405 | + except file_handler.NotAChangesFileError as exc: |
| 406 | log.error("Upload file must be a .changes; you provided " |
| 407 | "'{}'".format(exc)) |
| 408 | - except (ChangesFileException, ChangesParseError) as exc: |
| 409 | + except (file_handler.ChangesFileException, |
| 410 | + file_handler.ChangesParseError) as exc: |
| 411 | log.error("Problem parsing .changes file, are you sure it's a valid " |
| 412 | ".changes? ({})".format(str(exc))) |
| 413 | - except ChangesValidationError as exc: |
| 414 | + except file_handler.ChangesValidationError as exc: |
| 415 | log.error("Problem validating .changes file: {}".format(exc)) |
| 416 | - except UploadDirNotFound as exc: |
| 417 | + except file_handler.UploadDirNotFound as exc: |
| 418 | log.error("Directory with files to upload not found: {}".format(exc)) |
| 419 | - except FileToUploadNotFound as exc: |
| 420 | + except file_handler.FileToUploadNotFound as exc: |
| 421 | log.error("File to upload not found: {}. Maybe a wrong or missing " |
| 422 | "-f?".format(exc)) |
| 423 | except httplib.BadStatusLine as exc: |
| 424 | log.error("Server at {} replied with an empty response. Is 'ci_url' " |
| 425 | "pointing to the correct service?".format(utils.CI_URL)) |
| 426 | + except image.ImageObjectNotFound as exc: |
| 427 | + log.error("Image cannot be downloaded: {} ({})".format(exc.filename, |
| 428 | + str(exc))) |
| 429 | except urllib2.URLError as exc: |
| 430 | if isinstance(exc, urllib2.HTTPError): |
| 431 | reason = exc.reason |
| 432 | @@ -157,8 +165,8 @@ |
| 433 | log.error("Cannot reach the server at {}. Please, check your " |
| 434 | "configuration file ({}): is 'ci_url' correctly set? " |
| 435 | "Is the server up and reachable from this machine? " |
| 436 | - "({}).".format(utils.CI_URL, DEF_CFG, reason)) |
| 437 | - except DataStoreException as exc: |
| 438 | + "({}).".format(utils.CI_URL, utils.DEF_CFG, reason)) |
| 439 | + except data_store.DataStoreException as exc: |
| 440 | log.error("Data Store Error: {}".format(exc)) |
| 441 | except Exception: |
| 442 | log.exception('Unexpected exception') |
PASSED: Continuous integration, rev:404 s-jenkins. ubuntu- ci:8080/ job/uci- engine- ci/440/
http://
Executed test runs:
Click here to trigger a rebuild: s-jenkins. ubuntu- ci:8080/ job/uci- engine- ci/440/ rebuild
http://