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 40KB

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