Unified SDK for OpenStack
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.
 
 
 

575 lines
24 KiB

  1. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  2. # not use this file except in compliance with the License. You may obtain
  3. # a copy of the License at
  4. #
  5. # http://www.apache.org/licenses/LICENSE-2.0
  6. #
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  9. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  10. # License for the specific language governing permissions and limitations
  11. # under the License.
  12. from openstack.compute.v2 import metadata
  13. from openstack.image.v2 import image
  14. from openstack import exceptions
  15. from openstack import resource
  16. from openstack import utils
  17. CONSOLE_TYPE_ACTION_MAPPING = {
  18. 'novnc': 'os-getVNCConsole',
  19. 'xvpvnc': 'os-getVNCConsole',
  20. 'spice-html5': 'os-getSPICEConsole',
  21. 'rdp-html5': 'os-getRDPConsole',
  22. 'serial': 'os-getSerialConsole'
  23. }
  24. class Server(resource.Resource, metadata.MetadataMixin, resource.TagMixin):
  25. resource_key = 'server'
  26. resources_key = 'servers'
  27. base_path = '/servers'
  28. # capabilities
  29. allow_create = True
  30. allow_fetch = True
  31. allow_commit = True
  32. allow_delete = True
  33. allow_list = True
  34. _query_mapping = resource.QueryParameters(
  35. "auto_disk_config", "availability_zone",
  36. "created_at", "description", "flavor",
  37. "hostname", "image", "kernel_id", "key_name",
  38. "launch_index", "launched_at", "locked_by", "name",
  39. "node", "power_state", "progress", "project_id", "ramdisk_id",
  40. "reservation_id", "root_device_name",
  41. "status", "task_state", "terminated_at", "user_id",
  42. "vm_state",
  43. "sort_key", "sort_dir",
  44. access_ipv4="access_ip_v4",
  45. access_ipv6="access_ip_v6",
  46. has_config_drive="config_drive",
  47. deleted_only="deleted",
  48. compute_host="host",
  49. is_soft_deleted="soft_deleted",
  50. ipv4_address="ip",
  51. ipv6_address="ip6",
  52. changes_since="changes-since",
  53. changes_before="changes-before",
  54. id="uuid",
  55. all_projects="all_tenants",
  56. **resource.TagMixin._tag_query_parameters
  57. )
  58. _max_microversion = '2.72'
  59. #: A list of dictionaries holding links relevant to this server.
  60. links = resource.Body('links')
  61. access_ipv4 = resource.Body('accessIPv4')
  62. access_ipv6 = resource.Body('accessIPv6')
  63. #: A dictionary of addresses this server can be accessed through.
  64. #: The dictionary contains keys such as ``private`` and ``public``,
  65. #: each containing a list of dictionaries for addresses of that type.
  66. #: The addresses are contained in a dictionary with keys ``addr``
  67. #: and ``version``, which is either 4 or 6 depending on the protocol
  68. #: of the IP address. *Type: dict*
  69. addresses = resource.Body('addresses', type=dict)
  70. #: When a server is first created, it provides the administrator password.
  71. admin_password = resource.Body('adminPass')
  72. #: A list of an attached volumes. Each item in the list contains at least
  73. #: an "id" key to identify the specific volumes.
  74. attached_volumes = resource.Body(
  75. 'os-extended-volumes:volumes_attached')
  76. #: The name of the availability zone this server is a part of.
  77. availability_zone = resource.Body('OS-EXT-AZ:availability_zone')
  78. #: Enables fine grained control of the block device mapping for an
  79. #: instance. This is typically used for booting servers from volumes.
  80. block_device_mapping = resource.Body('block_device_mapping_v2')
  81. #: Indicates whether or not a config drive was used for this server.
  82. config_drive = resource.Body('config_drive')
  83. #: The name of the compute host on which this instance is running.
  84. #: Appears in the response for administrative users only.
  85. compute_host = resource.Body('OS-EXT-SRV-ATTR:host')
  86. #: Timestamp of when the server was created.
  87. created_at = resource.Body('created')
  88. #: The description of the server. Before microversion
  89. #: 2.19 this was set to the server name.
  90. description = resource.Body('description')
  91. #: The disk configuration. Either AUTO or MANUAL.
  92. disk_config = resource.Body('OS-DCF:diskConfig')
  93. #: The flavor reference, as a ID or full URL, for the flavor to use for
  94. #: this server.
  95. flavor_id = resource.Body('flavorRef')
  96. #: The flavor property as returned from server.
  97. # TODO(gtema): replace with flavor.Flavor addressing flavor.original_name
  98. flavor = resource.Body('flavor', type=dict)
  99. #: Indicates whether a configuration drive enables metadata injection.
  100. #: Not all cloud providers enable this feature.
  101. has_config_drive = resource.Body('config_drive')
  102. #: An ID representing the host of this server.
  103. host_id = resource.Body('hostId')
  104. #: The host status.
  105. host_status = resource.Body('host_status')
  106. #: The hostname set on the instance when it is booted.
  107. #: By default, it appears in the response for administrative users only.
  108. hostname = resource.Body('OS-EXT-SRV-ATTR:hostname')
  109. #: The hypervisor host name. Appears in the response for administrative
  110. #: users only.
  111. hypervisor_hostname = resource.Body('OS-EXT-SRV-ATTR:hypervisor_hostname')
  112. #: The image reference, as a ID or full URL, for the image to use for
  113. #: this server.
  114. image_id = resource.Body('imageRef')
  115. #: The image property as returned from server.
  116. image = resource.Body('image', type=image.Image)
  117. #: The instance name. The Compute API generates the instance name from the
  118. #: instance name template. Appears in the response for administrative users
  119. #: only.
  120. instance_name = resource.Body('OS-EXT-SRV-ATTR:instance_name')
  121. # The locked status of the server
  122. is_locked = resource.Body('locked', type=bool)
  123. #: The UUID of the kernel image when using an AMI. Will be null if not.
  124. #: By default, it appears in the response for administrative users only.
  125. kernel_id = resource.Body('OS-EXT-SRV-ATTR:kernel_id')
  126. #: The name of an associated keypair
  127. key_name = resource.Body('key_name')
  128. #: When servers are launched via multiple create, this is the
  129. #: sequence in which the servers were launched. By default, it
  130. #: appears in the response for administrative users only.
  131. launch_index = resource.Body('OS-EXT-SRV-ATTR:launch_index', type=int)
  132. #: The timestamp when the server was launched.
  133. launched_at = resource.Body('OS-SRV-USG:launched_at')
  134. #: Metadata stored for this server. *Type: dict*
  135. metadata = resource.Body('metadata', type=dict)
  136. #: A networks object. Required parameter when there are multiple
  137. #: networks defined for the tenant. When you do not specify the
  138. #: networks parameter, the server attaches to the only network
  139. #: created for the current tenant.
  140. networks = resource.Body('networks')
  141. #: The file path and contents, text only, to inject into the server at
  142. #: launch. The maximum size of the file path data is 255 bytes.
  143. #: The maximum limit is The number of allowed bytes in the decoded,
  144. #: rather than encoded, data.
  145. personality = resource.Body('personality')
  146. #: The power state of this server.
  147. power_state = resource.Body('OS-EXT-STS:power_state')
  148. #: While the server is building, this value represents the percentage
  149. #: of completion. Once it is completed, it will be 100. *Type: int*
  150. progress = resource.Body('progress', type=int)
  151. #: The ID of the project this server is associated with.
  152. project_id = resource.Body('tenant_id')
  153. #: The UUID of the ramdisk image when using an AMI. Will be null if not.
  154. #: By default, it appears in the response for administrative users only.
  155. ramdisk_id = resource.Body('OS-EXT-SRV-ATTR:ramdisk_id')
  156. #: The reservation id for the server. This is an id that can be
  157. #: useful in tracking groups of servers created with multiple create,
  158. #: that will all have the same reservation_id. By default, it appears
  159. #: in the response for administrative users only.
  160. reservation_id = resource.Body('OS-EXT-SRV-ATTR:reservation_id')
  161. #: The root device name for the instance By default, it appears in the
  162. #: response for administrative users only.
  163. root_device_name = resource.Body('OS-EXT-SRV-ATTR:root_device_name')
  164. #: The dictionary of data to send to the scheduler.
  165. scheduler_hints = resource.Body('OS-SCH-HNT:scheduler_hints', type=dict)
  166. #: A list of applicable security groups. Each group contains keys for
  167. #: description, name, id, and rules.
  168. security_groups = resource.Body('security_groups',
  169. type=list, list_type=dict)
  170. #: The UUIDs of the server groups to which the server belongs.
  171. #: Currently this can contain at most one entry.
  172. server_groups = resource.Body('server_groups', type=list)
  173. #: The state this server is in. Valid values include ``ACTIVE``,
  174. #: ``BUILDING``, ``DELETED``, ``ERROR``, ``HARD_REBOOT``, ``PASSWORD``,
  175. #: ``PAUSED``, ``REBOOT``, ``REBUILD``, ``RESCUED``, ``RESIZED``,
  176. #: ``REVERT_RESIZE``, ``SHUTOFF``, ``SOFT_DELETED``, ``STOPPED``,
  177. #: ``SUSPENDED``, ``UNKNOWN``, or ``VERIFY_RESIZE``.
  178. status = resource.Body('status')
  179. #: The task state of this server.
  180. task_state = resource.Body('OS-EXT-STS:task_state')
  181. #: The timestamp when the server was terminated (if it has been).
  182. terminated_at = resource.Body('OS-SRV-USG:terminated_at')
  183. #: A list of trusted certificate IDs, that were used during image
  184. #: signature verification to verify the signing certificate.
  185. trusted_image_certificates = resource.Body(
  186. 'trusted_image_certificates', type=list)
  187. #: Timestamp of when this server was last updated.
  188. updated_at = resource.Body('updated')
  189. #: Configuration information or scripts to use upon launch.
  190. #: Must be Base64 encoded.
  191. user_data = resource.Body('OS-EXT-SRV-ATTR:user_data')
  192. #: The ID of the owners of this server.
  193. user_id = resource.Body('user_id')
  194. #: The VM state of this server.
  195. vm_state = resource.Body('OS-EXT-STS:vm_state')
  196. def _prepare_request(self, requires_id=True, prepend_key=True,
  197. base_path=None, **kwargs):
  198. request = super(Server, self)._prepare_request(requires_id=requires_id,
  199. prepend_key=prepend_key,
  200. base_path=base_path)
  201. server_body = request.body[self.resource_key]
  202. # Some names exist without prefix on requests but with a prefix
  203. # on responses. If we find that we've populated one of these
  204. # attributes with something and then go to make a request, swap out
  205. # the name to the bare version.
  206. # Availability Zones exist with a prefix on response, but not request
  207. az_key = "OS-EXT-AZ:availability_zone"
  208. if az_key in server_body:
  209. server_body["availability_zone"] = server_body.pop(az_key)
  210. # User Data exists with a prefix on response, but not request
  211. ud_key = "OS-EXT-SRV-ATTR:user_data"
  212. if ud_key in server_body:
  213. server_body["user_data"] = server_body.pop(ud_key)
  214. # Scheduler hints are sent in a top-level scope, not within the
  215. # resource_key scope like everything else. If we try to send
  216. # scheduler_hints, pop them out of the resource_key scope and into
  217. # their own top-level scope.
  218. hint_key = "OS-SCH-HNT:scheduler_hints"
  219. if hint_key in server_body:
  220. request.body[hint_key] = server_body.pop(hint_key)
  221. return request
  222. def _action(self, session, body, microversion=None):
  223. """Preform server actions given the message body."""
  224. # NOTE: This is using Server.base_path instead of self.base_path
  225. # as both Server and ServerDetail instances can be acted on, but
  226. # the URL used is sans any additional /detail/ part.
  227. url = utils.urljoin(Server.base_path, self.id, 'action')
  228. headers = {'Accept': ''}
  229. response = session.post(
  230. url, json=body, headers=headers, microversion=microversion)
  231. exceptions.raise_from_response(response)
  232. return response
  233. def change_password(self, session, new_password):
  234. """Change the administrator password to the given password."""
  235. body = {'changePassword': {'adminPass': new_password}}
  236. self._action(session, body)
  237. def get_password(self, session):
  238. """Get the encrypted administrator password."""
  239. url = utils.urljoin(Server.base_path, self.id, 'os-server-password')
  240. return session.get(url)
  241. def reboot(self, session, reboot_type):
  242. """Reboot server where reboot_type might be 'SOFT' or 'HARD'."""
  243. body = {'reboot': {'type': reboot_type}}
  244. self._action(session, body)
  245. def force_delete(self, session):
  246. """Force delete a server."""
  247. body = {'forceDelete': None}
  248. self._action(session, body)
  249. def rebuild(self, session, name=None, admin_password=None,
  250. preserve_ephemeral=False, image=None,
  251. access_ipv4=None, access_ipv6=None,
  252. metadata=None, personality=None):
  253. """Rebuild the server with the given arguments."""
  254. action = {
  255. 'preserve_ephemeral': preserve_ephemeral
  256. }
  257. if image is not None:
  258. action['imageRef'] = resource.Resource._get_id(image)
  259. if name is not None:
  260. action['name'] = name
  261. if admin_password is not None:
  262. action['adminPass'] = admin_password
  263. if access_ipv4 is not None:
  264. action['accessIPv4'] = access_ipv4
  265. if access_ipv6 is not None:
  266. action['accessIPv6'] = access_ipv6
  267. if metadata is not None:
  268. action['metadata'] = metadata
  269. if personality is not None:
  270. action['personality'] = personality
  271. body = {'rebuild': action}
  272. response = self._action(session, body)
  273. self._translate_response(response)
  274. return self
  275. def resize(self, session, flavor):
  276. """Resize server to flavor reference."""
  277. body = {'resize': {'flavorRef': flavor}}
  278. self._action(session, body)
  279. def confirm_resize(self, session):
  280. """Confirm the resize of the server."""
  281. body = {'confirmResize': None}
  282. self._action(session, body)
  283. def revert_resize(self, session):
  284. """Revert the resize of the server."""
  285. body = {'revertResize': None}
  286. self._action(session, body)
  287. def create_image(self, session, name, metadata=None):
  288. """Create image from server."""
  289. action = {'name': name}
  290. if metadata is not None:
  291. action['metadata'] = metadata
  292. body = {'createImage': action}
  293. # You won't believe it - wait, who am I kidding - of course you will!
  294. # Nova returns the URL of the image created in the Location
  295. # header of the response. (what?) But, even better, the URL it responds
  296. # with has a very good chance of being wrong (it is built from
  297. # nova.conf values that point to internal API servers in any cloud
  298. # large enough to have both public and internal endpoints.
  299. # However, nobody has ever noticed this because novaclient doesn't
  300. # actually use that URL - it extracts the id from the end of
  301. # the url, then returns the id. This leads us to question:
  302. # a) why Nova is going to return a value in a header
  303. # b) why it's going to return data that probably broken
  304. # c) indeed the very nature of the fabric of reality
  305. # Although it fills us with existential dread, we have no choice but
  306. # to follow suit like a lemming being forced over a cliff by evil
  307. # producers from Disney.
  308. microversion = None
  309. if utils.supports_microversion(session, '2.45'):
  310. microversion = '2.45'
  311. response = self._action(session, body, microversion)
  312. body = None
  313. try:
  314. # There might be body, might be not
  315. body = response.json()
  316. except Exception:
  317. pass
  318. if body and 'image_id' in body:
  319. image_id = body['image_id']
  320. else:
  321. image_id = response.headers['Location'].rsplit('/', 1)[1]
  322. return image_id
  323. def add_security_group(self, session, security_group_name):
  324. body = {"addSecurityGroup": {"name": security_group_name}}
  325. self._action(session, body)
  326. def remove_security_group(self, session, security_group_name):
  327. body = {"removeSecurityGroup": {"name": security_group_name}}
  328. self._action(session, body)
  329. def reset_state(self, session, state):
  330. body = {"os-resetState": {"state": state}}
  331. self._action(session, body)
  332. def add_fixed_ip(self, session, network_id):
  333. body = {"addFixedIp": {"networkId": network_id}}
  334. self._action(session, body)
  335. def remove_fixed_ip(self, session, address):
  336. body = {"removeFixedIp": {"address": address}}
  337. self._action(session, body)
  338. def add_floating_ip(self, session, address, fixed_address=None):
  339. body = {"addFloatingIp": {"address": address}}
  340. if fixed_address is not None:
  341. body['addFloatingIp']['fixed_address'] = fixed_address
  342. self._action(session, body)
  343. def remove_floating_ip(self, session, address):
  344. body = {"removeFloatingIp": {"address": address}}
  345. self._action(session, body)
  346. def backup(self, session, name, backup_type, rotation):
  347. body = {
  348. "createBackup": {
  349. "name": name,
  350. "backup_type": backup_type,
  351. "rotation": rotation
  352. }
  353. }
  354. self._action(session, body)
  355. def pause(self, session):
  356. body = {"pause": None}
  357. self._action(session, body)
  358. def unpause(self, session):
  359. body = {"unpause": None}
  360. self._action(session, body)
  361. def suspend(self, session):
  362. body = {"suspend": None}
  363. self._action(session, body)
  364. def resume(self, session):
  365. body = {"resume": None}
  366. self._action(session, body)
  367. def lock(self, session):
  368. body = {"lock": None}
  369. self._action(session, body)
  370. def unlock(self, session):
  371. body = {"unlock": None}
  372. self._action(session, body)
  373. def rescue(self, session, admin_pass=None, image_ref=None):
  374. body = {"rescue": {}}
  375. if admin_pass is not None:
  376. body["rescue"]["adminPass"] = admin_pass
  377. if image_ref is not None:
  378. body["rescue"]["rescue_image_ref"] = image_ref
  379. self._action(session, body)
  380. def unrescue(self, session):
  381. body = {"unrescue": None}
  382. self._action(session, body)
  383. def evacuate(self, session, host=None, admin_pass=None, force=None):
  384. body = {"evacuate": {}}
  385. if host is not None:
  386. body["evacuate"]["host"] = host
  387. if admin_pass is not None:
  388. body["evacuate"]["adminPass"] = admin_pass
  389. if force is not None:
  390. body["evacuate"]["force"] = force
  391. self._action(session, body)
  392. def start(self, session):
  393. body = {"os-start": None}
  394. self._action(session, body)
  395. def stop(self, session):
  396. body = {"os-stop": None}
  397. self._action(session, body)
  398. def shelve(self, session):
  399. body = {"shelve": None}
  400. self._action(session, body)
  401. def unshelve(self, session):
  402. body = {"unshelve": None}
  403. self._action(session, body)
  404. def migrate(self, session):
  405. body = {"migrate": None}
  406. self._action(session, body)
  407. def get_console_output(self, session, length=None):
  408. body = {"os-getConsoleOutput": {}}
  409. if length is not None:
  410. body["os-getConsoleOutput"]["length"] = length
  411. resp = self._action(session, body)
  412. return resp.json()
  413. def live_migrate(self, session, host, force, block_migration,
  414. disk_over_commit=False):
  415. if utils.supports_microversion(session, '2.30'):
  416. return self._live_migrate_30(
  417. session, host,
  418. force=force,
  419. block_migration=block_migration)
  420. elif utils.supports_microversion(session, '2.25'):
  421. return self._live_migrate_25(
  422. session, host,
  423. force=force,
  424. block_migration=block_migration)
  425. else:
  426. return self._live_migrate(
  427. session, host,
  428. force=force,
  429. block_migration=block_migration,
  430. disk_over_commit=disk_over_commit)
  431. def get_console_url(self, session, console_type):
  432. action = CONSOLE_TYPE_ACTION_MAPPING.get(console_type)
  433. if not action:
  434. raise ValueError("Unsupported console type %s" % console_type)
  435. body = {action: {'type': console_type}}
  436. resp = self._action(session, body)
  437. return resp.json().get('console')
  438. def _live_migrate_30(self, session, host, force, block_migration):
  439. microversion = '2.30'
  440. body = {'host': None}
  441. if block_migration is None:
  442. block_migration = 'auto'
  443. body['block_migration'] = block_migration
  444. if host:
  445. body['host'] = host
  446. if force:
  447. body['force'] = force
  448. self._action(
  449. session, {'os-migrateLive': body}, microversion=microversion)
  450. def _live_migrate_25(self, session, host, force, block_migration):
  451. microversion = '2.25'
  452. body = {'host': None}
  453. if block_migration is None:
  454. block_migration = 'auto'
  455. body['block_migration'] = block_migration
  456. if host:
  457. body['host'] = host
  458. if not force:
  459. raise ValueError(
  460. "Live migration on this cloud implies 'force'"
  461. " if the 'host' option has been given and it is not"
  462. " possible to disable. It is recommended to not use 'host'"
  463. " at all on this cloud as it is inherently unsafe, but if"
  464. " it is unavoidable, please supply 'force=True' so that it"
  465. " is clear you understand the risks.")
  466. self._action(
  467. session, {'os-migrateLive': body}, microversion=microversion)
  468. def _live_migrate(self, session, host, force, block_migration,
  469. disk_over_commit):
  470. microversion = None
  471. body = {
  472. 'host': None,
  473. }
  474. if block_migration == 'auto':
  475. raise ValueError(
  476. "Live migration on this cloud does not support 'auto' as"
  477. " a parameter to block_migration, but only True and False.")
  478. body['block_migration'] = block_migration or False
  479. body['disk_over_commit'] = disk_over_commit or False
  480. if host:
  481. body['host'] = host
  482. if not force:
  483. raise ValueError(
  484. "Live migration on this cloud implies 'force'"
  485. " if the 'host' option has been given and it is not"
  486. " possible to disable. It is recommended to not use 'host'"
  487. " at all on this cloud as it is inherently unsafe, but if"
  488. " it is unavoidable, please supply 'force=True' so that it"
  489. " is clear you understand the risks.")
  490. self._action(
  491. session, {'os-migrateLive': body}, microversion=microversion)
  492. def fetch_security_groups(self, session):
  493. """Fetch security groups of a server.
  494. :returns: Updated Server instance.
  495. """
  496. url = utils.urljoin(Server.base_path, self.id, 'os-security-groups')
  497. response = session.get(url)
  498. exceptions.raise_from_response(response)
  499. try:
  500. data = response.json()
  501. if 'security_groups' in data:
  502. self.security_groups = data['security_groups']
  503. except ValueError:
  504. pass
  505. return self
  506. ServerDetail = Server