Client for OpenStack services
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

image.py 41KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179
  1. # Copyright 2012-2013 OpenStack Foundation
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  4. # not use this file except in compliance with the License. You may obtain
  5. # a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  11. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  12. # License for the specific language governing permissions and limitations
  13. # under the License.
  14. #
  15. """Image V2 Action Implementations"""
  16. import argparse
  17. from base64 import b64encode
  18. import logging
  19. from glanceclient.common import utils as gc_utils
  20. from openstack.image import image_signer
  21. from osc_lib.cli import parseractions
  22. from osc_lib.command import command
  23. from osc_lib import exceptions
  24. from osc_lib import utils
  25. import six
  26. from openstackclient.api import utils as api_utils
  27. from openstackclient.i18n import _
  28. from openstackclient.identity import common
  29. CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"]
  30. DEFAULT_CONTAINER_FORMAT = 'bare'
  31. DEFAULT_DISK_FORMAT = 'raw'
  32. DISK_CHOICES = ["ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vhdx",
  33. "vdi", "iso", "ploop"]
  34. MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"]
  35. LOG = logging.getLogger(__name__)
  36. def _format_image(image):
  37. """Format an image to make it more consistent with OSC operations."""
  38. info = {}
  39. properties = {}
  40. # the only fields we're not including is "links", "tags" and the properties
  41. fields_to_show = ['status', 'name', 'container_format', 'created_at',
  42. 'size', 'disk_format', 'updated_at', 'visibility',
  43. 'min_disk', 'protected', 'id', 'file', 'checksum',
  44. 'owner', 'virtual_size', 'min_ram', 'schema']
  45. # split out the usual key and the properties which are top-level
  46. for key in six.iterkeys(image):
  47. if key in fields_to_show:
  48. info[key] = image.get(key)
  49. elif key == 'tags':
  50. continue # handle this later
  51. else:
  52. properties[key] = image.get(key)
  53. # format the tags if they are there
  54. info['tags'] = utils.format_list(image.get('tags'))
  55. # add properties back into the dictionary as a top-level key
  56. if properties:
  57. info['properties'] = utils.format_dict(properties)
  58. return info
  59. class AddProjectToImage(command.ShowOne):
  60. _description = _("Associate project with image")
  61. def get_parser(self, prog_name):
  62. parser = super(AddProjectToImage, self).get_parser(prog_name)
  63. parser.add_argument(
  64. "image",
  65. metavar="<image>",
  66. help=_("Image to share (name or ID)"),
  67. )
  68. parser.add_argument(
  69. "project",
  70. metavar="<project>",
  71. help=_("Project to associate with image (ID)"),
  72. )
  73. common.add_project_domain_option_to_parser(parser)
  74. return parser
  75. def take_action(self, parsed_args):
  76. image_client = self.app.client_manager.image
  77. identity_client = self.app.client_manager.identity
  78. project_id = common.find_project(identity_client,
  79. parsed_args.project,
  80. parsed_args.project_domain).id
  81. image_id = utils.find_resource(
  82. image_client.images,
  83. parsed_args.image).id
  84. image_member = image_client.image_members.create(
  85. image_id,
  86. project_id,
  87. )
  88. return zip(*sorted(six.iteritems(image_member)))
  89. class CreateImage(command.ShowOne):
  90. _description = _("Create/upload an image")
  91. deadopts = ('size', 'location', 'copy-from', 'checksum', 'store')
  92. def get_parser(self, prog_name):
  93. parser = super(CreateImage, self).get_parser(prog_name)
  94. # TODO(bunting): There are additional arguments that v1 supported
  95. # that v2 either doesn't support or supports weirdly.
  96. # --checksum - could be faked clientside perhaps?
  97. # --location - maybe location add?
  98. # --size - passing image size is actually broken in python-glanceclient
  99. # --copy-from - does not exist in v2
  100. # --store - does not exits in v2
  101. parser.add_argument(
  102. "name",
  103. metavar="<image-name>",
  104. help=_("New image name"),
  105. )
  106. parser.add_argument(
  107. "--id",
  108. metavar="<id>",
  109. help=_("Image ID to reserve"),
  110. )
  111. parser.add_argument(
  112. "--container-format",
  113. default=DEFAULT_CONTAINER_FORMAT,
  114. choices=CONTAINER_CHOICES,
  115. metavar="<container-format>",
  116. help=(_("Image container format. "
  117. "The supported options are: %(option_list)s. "
  118. "The default format is: %(default_opt)s") %
  119. {'option_list': ', '.join(CONTAINER_CHOICES),
  120. 'default_opt': DEFAULT_CONTAINER_FORMAT})
  121. )
  122. parser.add_argument(
  123. "--disk-format",
  124. default=DEFAULT_DISK_FORMAT,
  125. choices=DISK_CHOICES,
  126. metavar="<disk-format>",
  127. help=_("Image disk format. The supported options are: %s. "
  128. "The default format is: raw") % ', '.join(DISK_CHOICES)
  129. )
  130. parser.add_argument(
  131. "--min-disk",
  132. metavar="<disk-gb>",
  133. type=int,
  134. help=_("Minimum disk size needed to boot image, in gigabytes"),
  135. )
  136. parser.add_argument(
  137. "--min-ram",
  138. metavar="<ram-mb>",
  139. type=int,
  140. help=_("Minimum RAM size needed to boot image, in megabytes"),
  141. )
  142. source_group = parser.add_mutually_exclusive_group()
  143. source_group.add_argument(
  144. "--file",
  145. metavar="<file>",
  146. help=_("Upload image from local file"),
  147. )
  148. source_group.add_argument(
  149. "--volume",
  150. metavar="<volume>",
  151. help=_("Create image from a volume"),
  152. )
  153. parser.add_argument(
  154. "--force",
  155. dest='force',
  156. action='store_true',
  157. default=False,
  158. help=_("Force image creation if volume is in use "
  159. "(only meaningful with --volume)"),
  160. )
  161. parser.add_argument(
  162. '--sign-key-path',
  163. metavar="<sign-key-path>",
  164. default=[],
  165. help=_("Sign the image using the specified private key. "
  166. "Only use in combination with --sign-cert-id")
  167. )
  168. parser.add_argument(
  169. '--sign-cert-id',
  170. metavar="<sign-cert-id>",
  171. default=[],
  172. help=_("The specified certificate UUID is a reference to "
  173. "the certificate in the key manager that corresponds "
  174. "to the public key and is used for signature validation. "
  175. "Only use in combination with --sign-key-path")
  176. )
  177. protected_group = parser.add_mutually_exclusive_group()
  178. protected_group.add_argument(
  179. "--protected",
  180. action="store_true",
  181. help=_("Prevent image from being deleted"),
  182. )
  183. protected_group.add_argument(
  184. "--unprotected",
  185. action="store_true",
  186. help=_("Allow image to be deleted (default)"),
  187. )
  188. public_group = parser.add_mutually_exclusive_group()
  189. public_group.add_argument(
  190. "--public",
  191. action="store_true",
  192. help=_("Image is accessible to the public"),
  193. )
  194. public_group.add_argument(
  195. "--private",
  196. action="store_true",
  197. help=_("Image is inaccessible to the public (default)"),
  198. )
  199. public_group.add_argument(
  200. "--community",
  201. action="store_true",
  202. help=_("Image is accessible to the community"),
  203. )
  204. public_group.add_argument(
  205. "--shared",
  206. action="store_true",
  207. help=_("Image can be shared"),
  208. )
  209. parser.add_argument(
  210. "--property",
  211. dest="properties",
  212. metavar="<key=value>",
  213. action=parseractions.KeyValueAction,
  214. help=_("Set a property on this image "
  215. "(repeat option to set multiple properties)"),
  216. )
  217. parser.add_argument(
  218. "--tag",
  219. dest="tags",
  220. metavar="<tag>",
  221. action='append',
  222. help=_("Set a tag on this image "
  223. "(repeat option to set multiple tags)"),
  224. )
  225. # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early
  226. # 2.x release. Do not remove before Jan 2017
  227. # and a 3.x release.
  228. project_group = parser.add_mutually_exclusive_group()
  229. project_group.add_argument(
  230. "--project",
  231. metavar="<project>",
  232. help=_("Set an alternate project on this image (name or ID)"),
  233. )
  234. project_group.add_argument(
  235. "--owner",
  236. metavar="<project>",
  237. help=argparse.SUPPRESS,
  238. )
  239. common.add_project_domain_option_to_parser(parser)
  240. for deadopt in self.deadopts:
  241. parser.add_argument(
  242. "--%s" % deadopt,
  243. metavar="<%s>" % deadopt,
  244. dest=deadopt.replace('-', '_'),
  245. help=argparse.SUPPRESS,
  246. )
  247. return parser
  248. def take_action(self, parsed_args):
  249. identity_client = self.app.client_manager.identity
  250. image_client = self.app.client_manager.image
  251. for deadopt in self.deadopts:
  252. if getattr(parsed_args, deadopt.replace('-', '_'), None):
  253. raise exceptions.CommandError(
  254. _("ERROR: --%s was given, which is an Image v1 option"
  255. " that is no longer supported in Image v2") % deadopt)
  256. # Build an attribute dict from the parsed args, only include
  257. # attributes that were actually set on the command line
  258. kwargs = {}
  259. copy_attrs = ('name', 'id',
  260. 'container_format', 'disk_format',
  261. 'min_disk', 'min_ram', 'tags', 'visibility')
  262. for attr in copy_attrs:
  263. if attr in parsed_args:
  264. val = getattr(parsed_args, attr, None)
  265. if val:
  266. # Only include a value in kwargs for attributes that
  267. # are actually present on the command line
  268. kwargs[attr] = val
  269. # properties should get flattened into the general kwargs
  270. if getattr(parsed_args, 'properties', None):
  271. for k, v in six.iteritems(parsed_args.properties):
  272. kwargs[k] = str(v)
  273. # Handle exclusive booleans with care
  274. # Avoid including attributes in kwargs if an option is not
  275. # present on the command line. These exclusive booleans are not
  276. # a single value for the pair of options because the default must be
  277. # to do nothing when no options are present as opposed to always
  278. # setting a default.
  279. if parsed_args.protected:
  280. kwargs['protected'] = True
  281. if parsed_args.unprotected:
  282. kwargs['protected'] = False
  283. if parsed_args.public:
  284. kwargs['visibility'] = 'public'
  285. if parsed_args.private:
  286. kwargs['visibility'] = 'private'
  287. if parsed_args.community:
  288. kwargs['visibility'] = 'community'
  289. if parsed_args.shared:
  290. kwargs['visibility'] = 'shared'
  291. # Handle deprecated --owner option
  292. project_arg = parsed_args.project
  293. if parsed_args.owner:
  294. project_arg = parsed_args.owner
  295. LOG.warning(_('The --owner option is deprecated, '
  296. 'please use --project instead.'))
  297. if project_arg:
  298. kwargs['owner'] = common.find_project(
  299. identity_client,
  300. project_arg,
  301. parsed_args.project_domain,
  302. ).id
  303. # open the file first to ensure any failures are handled before the
  304. # image is created
  305. fp = gc_utils.get_data_file(parsed_args)
  306. info = {}
  307. if fp is not None and parsed_args.volume:
  308. raise exceptions.CommandError(_("Uploading data and using "
  309. "container are not allowed at "
  310. "the same time"))
  311. if fp is None and parsed_args.file:
  312. LOG.warning(_("Failed to get an image file."))
  313. return {}, {}
  314. if parsed_args.owner:
  315. kwargs['owner'] = common.find_project(
  316. identity_client,
  317. parsed_args.owner,
  318. parsed_args.project_domain,
  319. ).id
  320. # sign an image using a given local private key file
  321. if parsed_args.sign_key_path or parsed_args.sign_cert_id:
  322. if not parsed_args.file:
  323. msg = (_("signing an image requires the --file option, "
  324. "passing files via stdin when signing is not "
  325. "supported."))
  326. raise exceptions.CommandError(msg)
  327. if (len(parsed_args.sign_key_path) < 1 or
  328. len(parsed_args.sign_cert_id) < 1):
  329. msg = (_("'sign-key-path' and 'sign-cert-id' must both be "
  330. "specified when attempting to sign an image."))
  331. raise exceptions.CommandError(msg)
  332. else:
  333. sign_key_path = parsed_args.sign_key_path
  334. sign_cert_id = parsed_args.sign_cert_id
  335. signer = image_signer.ImageSigner()
  336. try:
  337. pw = utils.get_password(
  338. self.app.stdin,
  339. prompt=("Please enter private key password, leave "
  340. "empty if none: "),
  341. confirm=False)
  342. if not pw or len(pw) < 1:
  343. pw = None
  344. signer.load_private_key(
  345. sign_key_path,
  346. password=pw)
  347. except Exception:
  348. msg = (_("Error during sign operation: private key could "
  349. "not be loaded."))
  350. raise exceptions.CommandError(msg)
  351. signature = signer.generate_signature(fp)
  352. signature_b64 = b64encode(signature)
  353. kwargs['img_signature'] = signature_b64
  354. kwargs['img_signature_certificate_uuid'] = sign_cert_id
  355. kwargs['img_signature_hash_method'] = signer.hash_method
  356. if signer.padding_method:
  357. kwargs['img_signature_key_type'] = signer.padding_method
  358. # If a volume is specified.
  359. if parsed_args.volume:
  360. volume_client = self.app.client_manager.volume
  361. source_volume = utils.find_resource(
  362. volume_client.volumes,
  363. parsed_args.volume,
  364. )
  365. response, body = volume_client.volumes.upload_to_image(
  366. source_volume.id,
  367. parsed_args.force,
  368. parsed_args.name,
  369. parsed_args.container_format,
  370. parsed_args.disk_format,
  371. )
  372. info = body['os-volume_upload_image']
  373. try:
  374. info['volume_type'] = info['volume_type']['name']
  375. except TypeError:
  376. info['volume_type'] = None
  377. else:
  378. image = image_client.images.create(**kwargs)
  379. if fp is not None:
  380. with fp:
  381. try:
  382. image_client.images.upload(image.id, fp)
  383. except Exception:
  384. # If the upload fails for some reason attempt to remove the
  385. # dangling queued image made by the create() call above but
  386. # only if the user did not specify an id which indicates
  387. # the Image already exists and should be left alone.
  388. try:
  389. if 'id' not in kwargs:
  390. image_client.images.delete(image.id)
  391. except Exception:
  392. pass # we don't care about this one
  393. raise # now, throw the upload exception again
  394. # update the image after the data has been uploaded
  395. image = image_client.images.get(image.id)
  396. if not info:
  397. info = _format_image(image)
  398. return zip(*sorted(six.iteritems(info)))
  399. class DeleteImage(command.Command):
  400. _description = _("Delete image(s)")
  401. def get_parser(self, prog_name):
  402. parser = super(DeleteImage, self).get_parser(prog_name)
  403. parser.add_argument(
  404. "images",
  405. metavar="<image>",
  406. nargs="+",
  407. help=_("Image(s) to delete (name or ID)"),
  408. )
  409. return parser
  410. def take_action(self, parsed_args):
  411. del_result = 0
  412. image_client = self.app.client_manager.image
  413. for image in parsed_args.images:
  414. try:
  415. image_obj = utils.find_resource(
  416. image_client.images,
  417. image,
  418. )
  419. image_client.images.delete(image_obj.id)
  420. except Exception as e:
  421. del_result += 1
  422. LOG.error(_("Failed to delete image with name or "
  423. "ID '%(image)s': %(e)s"),
  424. {'image': image, 'e': e})
  425. total = len(parsed_args.images)
  426. if (del_result > 0):
  427. msg = (_("Failed to delete %(dresult)s of %(total)s images.")
  428. % {'dresult': del_result, 'total': total})
  429. raise exceptions.CommandError(msg)
  430. class ListImage(command.Lister):
  431. _description = _("List available images")
  432. def get_parser(self, prog_name):
  433. parser = super(ListImage, self).get_parser(prog_name)
  434. public_group = parser.add_mutually_exclusive_group()
  435. public_group.add_argument(
  436. "--public",
  437. dest="public",
  438. action="store_true",
  439. default=False,
  440. help=_("List only public images"),
  441. )
  442. public_group.add_argument(
  443. "--private",
  444. dest="private",
  445. action="store_true",
  446. default=False,
  447. help=_("List only private images"),
  448. )
  449. public_group.add_argument(
  450. "--community",
  451. dest="community",
  452. action="store_true",
  453. default=False,
  454. help=_("List only community images"),
  455. )
  456. public_group.add_argument(
  457. "--shared",
  458. dest="shared",
  459. action="store_true",
  460. default=False,
  461. help=_("List only shared images"),
  462. )
  463. parser.add_argument(
  464. '--property',
  465. metavar='<key=value>',
  466. action=parseractions.KeyValueAction,
  467. help=_('Filter output based on property '
  468. '(repeat option to filter on multiple properties)'),
  469. )
  470. parser.add_argument(
  471. '--name',
  472. metavar='<name>',
  473. default=None,
  474. help=_("Filter images based on name.")
  475. )
  476. parser.add_argument(
  477. '--status',
  478. metavar='<status>',
  479. default=None,
  480. help=_("Filter images based on status.")
  481. )
  482. parser.add_argument(
  483. '--member-status',
  484. metavar='<member-status>',
  485. default=None,
  486. type=lambda s: s.lower(),
  487. choices=MEMBER_STATUS_CHOICES,
  488. help=(_("Filter images based on member status. "
  489. "The supported options are: %s. ") %
  490. ', '.join(MEMBER_STATUS_CHOICES))
  491. )
  492. parser.add_argument(
  493. '--tag',
  494. metavar='<tag>',
  495. default=None,
  496. help=_('Filter images based on tag.'),
  497. )
  498. parser.add_argument(
  499. '--long',
  500. action='store_true',
  501. default=False,
  502. help=_('List additional fields in output'),
  503. )
  504. # --page-size has never worked, leave here for silent compatibility
  505. # We'll implement limit/marker differently later
  506. parser.add_argument(
  507. "--page-size",
  508. metavar="<size>",
  509. help=argparse.SUPPRESS,
  510. )
  511. parser.add_argument(
  512. '--sort',
  513. metavar="<key>[:<direction>]",
  514. default='name:asc',
  515. help=_("Sort output by selected keys and directions(asc or desc) "
  516. "(default: name:asc), multiple keys and directions can be "
  517. "specified separated by comma"),
  518. )
  519. parser.add_argument(
  520. "--limit",
  521. metavar="<num-images>",
  522. type=int,
  523. help=_("Maximum number of images to display."),
  524. )
  525. parser.add_argument(
  526. '--marker',
  527. metavar='<image>',
  528. default=None,
  529. help=_("The last image of the previous page. Display "
  530. "list of images after marker. Display all images if not "
  531. "specified. (name or ID)"),
  532. )
  533. return parser
  534. def take_action(self, parsed_args):
  535. image_client = self.app.client_manager.image
  536. kwargs = {}
  537. if parsed_args.public:
  538. kwargs['public'] = True
  539. if parsed_args.private:
  540. kwargs['private'] = True
  541. if parsed_args.community:
  542. kwargs['community'] = True
  543. if parsed_args.shared:
  544. kwargs['shared'] = True
  545. if parsed_args.limit:
  546. kwargs['limit'] = parsed_args.limit
  547. if parsed_args.marker:
  548. kwargs['marker'] = utils.find_resource(image_client.images,
  549. parsed_args.marker).id
  550. if parsed_args.name:
  551. kwargs['name'] = parsed_args.name
  552. if parsed_args.status:
  553. kwargs['status'] = parsed_args.status
  554. if parsed_args.member_status:
  555. kwargs['member_status'] = parsed_args.member_status
  556. if parsed_args.tag:
  557. kwargs['tag'] = parsed_args.tag
  558. if parsed_args.long:
  559. columns = (
  560. 'ID',
  561. 'Name',
  562. 'Disk Format',
  563. 'Container Format',
  564. 'Size',
  565. 'Checksum',
  566. 'Status',
  567. 'visibility',
  568. 'protected',
  569. 'owner',
  570. 'tags',
  571. )
  572. column_headers = (
  573. 'ID',
  574. 'Name',
  575. 'Disk Format',
  576. 'Container Format',
  577. 'Size',
  578. 'Checksum',
  579. 'Status',
  580. 'Visibility',
  581. 'Protected',
  582. 'Project',
  583. 'Tags',
  584. )
  585. else:
  586. columns = ("ID", "Name", "Status")
  587. column_headers = columns
  588. # List of image data received
  589. data = []
  590. if 'marker' in kwargs:
  591. data = image_client.api.image_list(**kwargs)
  592. else:
  593. # No pages received yet, so start the page marker at None.
  594. marker = None
  595. while True:
  596. page = image_client.api.image_list(marker=marker, **kwargs)
  597. if not page:
  598. break
  599. data.extend(page)
  600. # Set the marker to the id of the last item we received
  601. marker = page[-1]['id']
  602. if parsed_args.property:
  603. for attr, value in parsed_args.property.items():
  604. api_utils.simple_filter(
  605. data,
  606. attr=attr,
  607. value=value,
  608. property_field='properties',
  609. )
  610. data = utils.sort_items(data, parsed_args.sort, str)
  611. return (
  612. column_headers,
  613. (utils.get_dict_properties(
  614. s,
  615. columns,
  616. formatters={
  617. 'tags': utils.format_list,
  618. },
  619. ) for s in data)
  620. )
  621. class ListImageProjects(command.Lister):
  622. _description = _("List projects associated with image")
  623. def get_parser(self, prog_name):
  624. parser = super(ListImageProjects, self).get_parser(prog_name)
  625. parser.add_argument(
  626. "image",
  627. metavar="<image>",
  628. help=_("Image (name or ID)"),
  629. )
  630. common.add_project_domain_option_to_parser(parser)
  631. return parser
  632. def take_action(self, parsed_args):
  633. image_client = self.app.client_manager.image
  634. columns = (
  635. "Image ID",
  636. "Member ID",
  637. "Status"
  638. )
  639. image_id = utils.find_resource(
  640. image_client.images,
  641. parsed_args.image).id
  642. data = image_client.image_members.list(image_id)
  643. return (columns,
  644. (utils.get_item_properties(
  645. s, columns,
  646. ) for s in data))
  647. class RemoveProjectImage(command.Command):
  648. _description = _("Disassociate project with image")
  649. def get_parser(self, prog_name):
  650. parser = super(RemoveProjectImage, self).get_parser(prog_name)
  651. parser.add_argument(
  652. "image",
  653. metavar="<image>",
  654. help=_("Image to unshare (name or ID)"),
  655. )
  656. parser.add_argument(
  657. "project",
  658. metavar="<project>",
  659. help=_("Project to disassociate with image (name or ID)"),
  660. )
  661. common.add_project_domain_option_to_parser(parser)
  662. return parser
  663. def take_action(self, parsed_args):
  664. image_client = self.app.client_manager.image
  665. identity_client = self.app.client_manager.identity
  666. project_id = common.find_project(identity_client,
  667. parsed_args.project,
  668. parsed_args.project_domain).id
  669. image_id = utils.find_resource(
  670. image_client.images,
  671. parsed_args.image).id
  672. image_client.image_members.delete(image_id, project_id)
  673. class SaveImage(command.Command):
  674. _description = _("Save an image locally")
  675. def get_parser(self, prog_name):
  676. parser = super(SaveImage, self).get_parser(prog_name)
  677. parser.add_argument(
  678. "--file",
  679. metavar="<filename>",
  680. help=_("Downloaded image save filename (default: stdout)"),
  681. )
  682. parser.add_argument(
  683. "image",
  684. metavar="<image>",
  685. help=_("Image to save (name or ID)"),
  686. )
  687. return parser
  688. def take_action(self, parsed_args):
  689. image_client = self.app.client_manager.image
  690. image = utils.find_resource(
  691. image_client.images,
  692. parsed_args.image,
  693. )
  694. data = image_client.images.data(image.id)
  695. if data.wrapped is None:
  696. msg = _('Image %s has no data.') % image.id
  697. LOG.error(msg)
  698. self.app.stdout.write(msg + '\n')
  699. raise SystemExit
  700. gc_utils.save_image(data, parsed_args.file)
  701. class SetImage(command.Command):
  702. _description = _("Set image properties")
  703. deadopts = ('visibility',)
  704. def get_parser(self, prog_name):
  705. parser = super(SetImage, self).get_parser(prog_name)
  706. # TODO(bunting): There are additional arguments that v1 supported
  707. # --size - does not exist in v2
  708. # --store - does not exist in v2
  709. # --location - maybe location add?
  710. # --copy-from - does not exist in v2
  711. # --file - should be able to upload file
  712. # --volume - not possible with v2 as can't change id
  713. # --force - see `--volume`
  714. # --checksum - maybe could be done client side
  715. # --stdin - could be implemented
  716. parser.add_argument(
  717. "image",
  718. metavar="<image>",
  719. help=_("Image to modify (name or ID)")
  720. )
  721. parser.add_argument(
  722. "--name",
  723. metavar="<name>",
  724. help=_("New image name")
  725. )
  726. parser.add_argument(
  727. "--min-disk",
  728. type=int,
  729. metavar="<disk-gb>",
  730. help=_("Minimum disk size needed to boot image, in gigabytes")
  731. )
  732. parser.add_argument(
  733. "--min-ram",
  734. type=int,
  735. metavar="<ram-mb>",
  736. help=_("Minimum RAM size needed to boot image, in megabytes"),
  737. )
  738. parser.add_argument(
  739. "--container-format",
  740. metavar="<container-format>",
  741. choices=CONTAINER_CHOICES,
  742. help=_("Image container format. The supported options are: %s") %
  743. ', '.join(CONTAINER_CHOICES)
  744. )
  745. parser.add_argument(
  746. "--disk-format",
  747. metavar="<disk-format>",
  748. choices=DISK_CHOICES,
  749. help=_("Image disk format. The supported options are: %s") %
  750. ', '.join(DISK_CHOICES)
  751. )
  752. protected_group = parser.add_mutually_exclusive_group()
  753. protected_group.add_argument(
  754. "--protected",
  755. action="store_true",
  756. help=_("Prevent image from being deleted"),
  757. )
  758. protected_group.add_argument(
  759. "--unprotected",
  760. action="store_true",
  761. help=_("Allow image to be deleted (default)"),
  762. )
  763. public_group = parser.add_mutually_exclusive_group()
  764. public_group.add_argument(
  765. "--public",
  766. action="store_true",
  767. help=_("Image is accessible to the public"),
  768. )
  769. public_group.add_argument(
  770. "--private",
  771. action="store_true",
  772. help=_("Image is inaccessible to the public (default)"),
  773. )
  774. public_group.add_argument(
  775. "--community",
  776. action="store_true",
  777. help=_("Image is accessible to the community"),
  778. )
  779. public_group.add_argument(
  780. "--shared",
  781. action="store_true",
  782. help=_("Image can be shared"),
  783. )
  784. parser.add_argument(
  785. "--property",
  786. dest="properties",
  787. metavar="<key=value>",
  788. action=parseractions.KeyValueAction,
  789. help=_("Set a property on this image "
  790. "(repeat option to set multiple properties)"),
  791. )
  792. parser.add_argument(
  793. "--tag",
  794. dest="tags",
  795. metavar="<tag>",
  796. default=None,
  797. action='append',
  798. help=_("Set a tag on this image "
  799. "(repeat option to set multiple tags)"),
  800. )
  801. parser.add_argument(
  802. "--architecture",
  803. metavar="<architecture>",
  804. help=_("Operating system architecture"),
  805. )
  806. parser.add_argument(
  807. "--instance-id",
  808. metavar="<instance-id>",
  809. help=_("ID of server instance used to create this image"),
  810. )
  811. parser.add_argument(
  812. "--instance-uuid",
  813. metavar="<instance-id>",
  814. dest="instance_id",
  815. help=argparse.SUPPRESS,
  816. )
  817. parser.add_argument(
  818. "--kernel-id",
  819. metavar="<kernel-id>",
  820. help=_("ID of kernel image used to boot this disk image"),
  821. )
  822. parser.add_argument(
  823. "--os-distro",
  824. metavar="<os-distro>",
  825. help=_("Operating system distribution name"),
  826. )
  827. parser.add_argument(
  828. "--os-version",
  829. metavar="<os-version>",
  830. help=_("Operating system distribution version"),
  831. )
  832. parser.add_argument(
  833. "--ramdisk-id",
  834. metavar="<ramdisk-id>",
  835. help=_("ID of ramdisk image used to boot this disk image"),
  836. )
  837. deactivate_group = parser.add_mutually_exclusive_group()
  838. deactivate_group.add_argument(
  839. "--deactivate",
  840. action="store_true",
  841. help=_("Deactivate the image"),
  842. )
  843. deactivate_group.add_argument(
  844. "--activate",
  845. action="store_true",
  846. help=_("Activate the image"),
  847. )
  848. # NOTE(dtroyer): --owner is deprecated in Jan 2016 in an early
  849. # 2.x release. Do not remove before Jan 2017
  850. # and a 3.x release.
  851. project_group = parser.add_mutually_exclusive_group()
  852. project_group.add_argument(
  853. "--project",
  854. metavar="<project>",
  855. help=_("Set an alternate project on this image (name or ID)"),
  856. )
  857. project_group.add_argument(
  858. "--owner",
  859. metavar="<project>",
  860. help=argparse.SUPPRESS,
  861. )
  862. common.add_project_domain_option_to_parser(parser)
  863. for deadopt in self.deadopts:
  864. parser.add_argument(
  865. "--%s" % deadopt,
  866. metavar="<%s>" % deadopt,
  867. dest=deadopt.replace('-', '_'),
  868. help=argparse.SUPPRESS,
  869. )
  870. membership_group = parser.add_mutually_exclusive_group()
  871. membership_group.add_argument(
  872. "--accept",
  873. action="store_true",
  874. help=_("Accept the image membership"),
  875. )
  876. membership_group.add_argument(
  877. "--reject",
  878. action="store_true",
  879. help=_("Reject the image membership"),
  880. )
  881. membership_group.add_argument(
  882. "--pending",
  883. action="store_true",
  884. help=_("Reset the image membership to 'pending'"),
  885. )
  886. return parser
  887. def take_action(self, parsed_args):
  888. identity_client = self.app.client_manager.identity
  889. image_client = self.app.client_manager.image
  890. for deadopt in self.deadopts:
  891. if getattr(parsed_args, deadopt.replace('-', '_'), None):
  892. raise exceptions.CommandError(
  893. _("ERROR: --%s was given, which is an Image v1 option"
  894. " that is no longer supported in Image v2") % deadopt)
  895. kwargs = {}
  896. copy_attrs = ('architecture', 'container_format', 'disk_format',
  897. 'file', 'instance_id', 'kernel_id', 'locations',
  898. 'min_disk', 'min_ram', 'name', 'os_distro', 'os_version',
  899. 'prefix', 'progress', 'ramdisk_id', 'tags', 'visibility')
  900. for attr in copy_attrs:
  901. if attr in parsed_args:
  902. val = getattr(parsed_args, attr, None)
  903. if val is not None:
  904. # Only include a value in kwargs for attributes that are
  905. # actually present on the command line
  906. kwargs[attr] = val
  907. # Properties should get flattened into the general kwargs
  908. if getattr(parsed_args, 'properties', None):
  909. for k, v in six.iteritems(parsed_args.properties):
  910. kwargs[k] = str(v)
  911. # Handle exclusive booleans with care
  912. # Avoid including attributes in kwargs if an option is not
  913. # present on the command line. These exclusive booleans are not
  914. # a single value for the pair of options because the default must be
  915. # to do nothing when no options are present as opposed to always
  916. # setting a default.
  917. if parsed_args.protected:
  918. kwargs['protected'] = True
  919. if parsed_args.unprotected:
  920. kwargs['protected'] = False
  921. if parsed_args.public:
  922. kwargs['visibility'] = 'public'
  923. if parsed_args.private:
  924. kwargs['visibility'] = 'private'
  925. if parsed_args.community:
  926. kwargs['visibility'] = 'community'
  927. if parsed_args.shared:
  928. kwargs['visibility'] = 'shared'
  929. # Handle deprecated --owner option
  930. project_arg = parsed_args.project
  931. if parsed_args.owner:
  932. project_arg = parsed_args.owner
  933. LOG.warning(_('The --owner option is deprecated, '
  934. 'please use --project instead.'))
  935. project_id = None
  936. if project_arg:
  937. project_id = common.find_project(
  938. identity_client,
  939. project_arg,
  940. parsed_args.project_domain,
  941. ).id
  942. kwargs['owner'] = project_id
  943. image = utils.find_resource(
  944. image_client.images, parsed_args.image)
  945. activation_status = None
  946. if parsed_args.deactivate:
  947. image_client.images.deactivate(image.id)
  948. activation_status = "deactivated"
  949. if parsed_args.activate:
  950. image_client.images.reactivate(image.id)
  951. activation_status = "activated"
  952. membership_group_args = ('accept', 'reject', 'pending')
  953. membership_status = [status for status in membership_group_args
  954. if getattr(parsed_args, status)]
  955. if membership_status:
  956. # If a specific project is not passed, assume we want to update
  957. # our own membership
  958. if not project_id:
  959. project_id = self.app.client_manager.auth_ref.project_id
  960. # The mutually exclusive group of the arg parser ensure we have at
  961. # most one item in the membership_status list.
  962. if membership_status[0] != 'pending':
  963. membership_status[0] += 'ed' # Glance expects the past form
  964. image_client.image_members.update(
  965. image.id, project_id, membership_status[0])
  966. if parsed_args.tags:
  967. # Tags should be extended, but duplicates removed
  968. kwargs['tags'] = list(set(image.tags).union(set(parsed_args.tags)))
  969. try:
  970. image = image_client.images.update(image.id, **kwargs)
  971. except Exception:
  972. if activation_status is not None:
  973. LOG.info(_("Image %(id)s was %(status)s."),
  974. {'id': image.id, 'status': activation_status})
  975. raise
  976. class ShowImage(command.ShowOne):
  977. _description = _("Display image details")
  978. def get_parser(self, prog_name):
  979. parser = super(ShowImage, self).get_parser(prog_name)
  980. parser.add_argument(
  981. "--human-readable",
  982. default=False,
  983. action='store_true',
  984. help=_("Print image size in a human-friendly format."),
  985. )
  986. parser.add_argument(
  987. "image",
  988. metavar="<image>",
  989. help=_("Image to display (name or ID)"),
  990. )
  991. return parser
  992. def take_action(self, parsed_args):
  993. image_client = self.app.client_manager.image
  994. image = utils.find_resource(
  995. image_client.images,
  996. parsed_args.image,
  997. )
  998. if parsed_args.human_readable:
  999. image['size'] = utils.format_size(image['size'])
  1000. info = _format_image(image)
  1001. return zip(*sorted(six.iteritems(info)))
  1002. class UnsetImage(command.Command):
  1003. _description = _("Unset image tags and properties")
  1004. def get_parser(self, prog_name):
  1005. parser = super(UnsetImage, self).get_parser(prog_name)
  1006. parser.add_argument(
  1007. "image",
  1008. metavar="<image>",
  1009. help=_("Image to modify (name or ID)"),
  1010. )
  1011. parser.add_argument(
  1012. "--tag",
  1013. dest="tags",
  1014. metavar="<tag>",
  1015. default=[],
  1016. action='append',
  1017. help=_("Unset a tag on this image "
  1018. "(repeat option to unset multiple tags)"),
  1019. )
  1020. parser.add_argument(
  1021. "--property",
  1022. dest="properties",
  1023. metavar="<property-key>",
  1024. default=[],
  1025. action='append',
  1026. help=_("Unset a property on this image "
  1027. "(repeat option to unset multiple properties)"),
  1028. )
  1029. return parser
  1030. def take_action(self, parsed_args):
  1031. image_client = self.app.client_manager.image
  1032. image = utils.find_resource(
  1033. image_client.images,
  1034. parsed_args.image,
  1035. )
  1036. kwargs = {}
  1037. tagret = 0
  1038. propret = 0
  1039. if parsed_args.tags:
  1040. for k in parsed_args.tags:
  1041. try:
  1042. image_client.image_tags.delete(image.id, k)
  1043. except Exception:
  1044. LOG.error(_("tag unset failed, '%s' is a "
  1045. "nonexistent tag "), k)
  1046. tagret += 1
  1047. if parsed_args.properties:
  1048. for k in parsed_args.properties:
  1049. if k not in image:
  1050. LOG.error(_("property unset failed, '%s' is a "
  1051. "nonexistent property "), k)
  1052. propret += 1
  1053. image_client.images.update(
  1054. image.id,
  1055. parsed_args.properties,
  1056. **kwargs)
  1057. tagtotal = len(parsed_args.tags)
  1058. proptotal = len(parsed_args.properties)
  1059. if (tagret > 0 and propret > 0):
  1060. msg = (_("Failed to unset %(tagret)s of %(tagtotal)s tags,"
  1061. "Failed to unset %(propret)s of %(proptotal)s properties.")
  1062. % {'tagret': tagret, 'tagtotal': tagtotal,
  1063. 'propret': propret, 'proptotal': proptotal})
  1064. raise exceptions.CommandError(msg)
  1065. elif tagret > 0:
  1066. msg = (_("Failed to unset %(tagret)s of %(tagtotal)s tags.")
  1067. % {'tagret': tagret, 'tagtotal': tagtotal})
  1068. raise exceptions.CommandError(msg)
  1069. elif propret > 0:
  1070. msg = (_("Failed to unset %(propret)s of %(proptotal)s"
  1071. " properties.")
  1072. % {'propret': propret, 'proptotal': proptotal})
  1073. raise exceptions.CommandError(msg)