blob: 43de4ef85f2585e6680ca31d1fa1f9ca3c4fbc5e [file] [log] [blame]
ivan-zhu09111942013-08-01 08:09:16 +08001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2#
3# Copyright 2012 IBM Corp.
4# Copyright 2013 Hewlett-Packard Development Company, L.P.
5# All Rights Reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License"); you may
8# not use this file except in compliance with the License. You may obtain
9# a copy of the License at
10#
11# http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16# License for the specific language governing permissions and limitations
17# under the License.
18
19import time
20import urllib
21
22from lxml import etree
23
24from tempest.common.rest_client import RestClientXML
25from tempest.common import waiters
26from tempest import exceptions
27from tempest.openstack.common import log as logging
28from tempest.services.compute.xml.common import Document
29from tempest.services.compute.xml.common import Element
30from tempest.services.compute.xml.common import Text
31from tempest.services.compute.xml.common import xml_to_json
32from tempest.services.compute.xml.common import XMLNS_11
33
34
35LOG = logging.getLogger(__name__)
36
37
38def _translate_ip_xml_json(ip):
39 """
40 Convert the address version to int.
41 """
42 ip = dict(ip)
43 version = ip.get('version')
44 if version:
45 ip['version'] = int(version)
46 # NOTE(maurosr): just a fast way to avoid the xml version with the
47 # expanded xml namespace.
48 type_ns_prefix = ('{http://docs.openstack.org/compute/ext/extended_ips/'
49 'api/v1.1}type')
50 mac_ns_prefix = ('{http://docs.openstack.org/compute/ext/extended_ips_mac'
51 '/api/v1.1}mac_addr')
52
53 if type_ns_prefix in ip:
54 ip['OS-EXT-IPS:type'] = ip.pop(type_ns_prefix)
55
56 if mac_ns_prefix in ip:
57 ip['OS-EXT-IPS-MAC:mac_addr'] = ip.pop(mac_ns_prefix)
58 return ip
59
60
61def _translate_network_xml_to_json(network):
62 return [_translate_ip_xml_json(ip.attrib)
63 for ip in network.findall('{%s}ip' % XMLNS_11)]
64
65
66def _translate_addresses_xml_to_json(xml_addresses):
67 return dict((network.attrib['id'], _translate_network_xml_to_json(network))
68 for network in xml_addresses.findall('{%s}network' % XMLNS_11))
69
70
71def _translate_server_xml_to_json(xml_dom):
72 """Convert server XML to server JSON.
73
74 The addresses collection does not convert well by the dumb xml_to_json.
75 This method does some pre and post-processing to deal with that.
76
77 Translate XML addresses subtree to JSON.
78
79 Having xml_doc similar to
80 <api:server xmlns:api="http://docs.openstack.org/compute/api/v1.1">
81 <api:addresses>
82 <api:network id="foo_novanetwork">
83 <api:ip version="4" addr="192.168.0.4"/>
84 </api:network>
85 <api:network id="bar_novanetwork">
86 <api:ip version="4" addr="10.1.0.4"/>
87 <api:ip version="6" addr="2001:0:0:1:2:3:4:5"/>
88 </api:network>
89 </api:addresses>
90 </api:server>
91
92 the _translate_server_xml_to_json(etree.fromstring(xml_doc)) should produce
93 something like
94
95 {'addresses': {'bar_novanetwork': [{'addr': '10.1.0.4', 'version': 4},
96 {'addr': '2001:0:0:1:2:3:4:5',
97 'version': 6}],
98 'foo_novanetwork': [{'addr': '192.168.0.4', 'version': 4}]}}
99 """
100 nsmap = {'api': XMLNS_11}
101 addresses = xml_dom.xpath('/api:server/api:addresses', namespaces=nsmap)
102 if addresses:
103 if len(addresses) > 1:
104 raise ValueError('Expected only single `addresses` element.')
105 json_addresses = _translate_addresses_xml_to_json(addresses[0])
106 json = xml_to_json(xml_dom)
107 json['addresses'] = json_addresses
108 else:
109 json = xml_to_json(xml_dom)
110 diskConfig = ('{http://docs.openstack.org'
111 '/compute/ext/disk_config/api/v1.1}diskConfig')
112 terminated_at = ('{http://docs.openstack.org/'
113 'compute/ext/server_usage/api/v1.1}terminated_at')
114 launched_at = ('{http://docs.openstack.org'
115 '/compute/ext/server_usage/api/v1.1}launched_at')
116 power_state = ('{http://docs.openstack.org'
117 '/compute/ext/extended_status/api/v1.1}power_state')
118 availability_zone = ('{http://docs.openstack.org'
119 '/compute/ext/extended_availability_zone/api/v2}'
120 'availability_zone')
121 vm_state = ('{http://docs.openstack.org'
122 '/compute/ext/extended_status/api/v1.1}vm_state')
123 task_state = ('{http://docs.openstack.org'
124 '/compute/ext/extended_status/api/v1.1}task_state')
125 if diskConfig in json:
126 json['OS-DCF:diskConfig'] = json.pop(diskConfig)
127 if terminated_at in json:
128 json['OS-SRV-USG:terminated_at'] = json.pop(terminated_at)
129 if launched_at in json:
130 json['OS-SRV-USG:launched_at'] = json.pop(launched_at)
131 if power_state in json:
132 json['OS-EXT-STS:power_state'] = json.pop(power_state)
133 if availability_zone in json:
134 json['OS-EXT-AZ:availability_zone'] = json.pop(availability_zone)
135 if vm_state in json:
136 json['OS-EXT-STS:vm_state'] = json.pop(vm_state)
137 if task_state in json:
138 json['OS-EXT-STS:task_state'] = json.pop(task_state)
139 return json
140
141
142class ServersClientXML(RestClientXML):
143
144 def __init__(self, config, username, password, auth_url, tenant_name=None,
145 auth_version='v2'):
146 super(ServersClientXML, self).__init__(config, username, password,
147 auth_url, tenant_name,
148 auth_version=auth_version)
149 self.service = self.config.compute.catalog_type
150
151 def _parse_key_value(self, node):
152 """Parse <foo key='key'>value</foo> data into {'key': 'value'}."""
153 data = {}
154 for node in node.getchildren():
155 data[node.get('key')] = node.text
156 return data
157
158 def _parse_links(self, node, json):
159 del json['link']
160 json['links'] = []
161 for linknode in node.findall('{http://www.w3.org/2005/Atom}link'):
162 json['links'].append(xml_to_json(linknode))
163
164 def _parse_server(self, body):
165 json = _translate_server_xml_to_json(body)
166
167 if 'metadata' in json and json['metadata']:
168 # NOTE(danms): if there was metadata, we need to re-parse
169 # that as a special type
170 metadata_tag = body.find('{%s}metadata' % XMLNS_11)
171 json["metadata"] = self._parse_key_value(metadata_tag)
172 if 'link' in json:
173 self._parse_links(body, json)
174 for sub in ['image', 'flavor']:
175 if sub in json and 'link' in json[sub]:
176 self._parse_links(body, json[sub])
177 return json
178
179 def _parse_xml_virtual_interfaces(self, xml_dom):
180 """
181 Return server's virtual interfaces XML as JSON.
182 """
183 data = {"virtual_interfaces": []}
184 for iface in xml_dom.getchildren():
185 data["virtual_interfaces"].append(
186 {"id": iface.get("id"),
187 "mac_address": iface.get("mac_address")})
188 return data
189
190 def get_server(self, server_id):
191 """Returns the details of an existing server."""
192 resp, body = self.get("servers/%s" % str(server_id), self.headers)
193 server = self._parse_server(etree.fromstring(body))
194 return resp, server
195
196 def lock_server(self, server_id, **kwargs):
197 """Locks the given server."""
198 return self.action(server_id, 'lock', None, **kwargs)
199
200 def unlock_server(self, server_id, **kwargs):
201 """Unlocks the given server."""
202 return self.action(server_id, 'unlock', None, **kwargs)
203
204 def suspend_server(self, server_id, **kwargs):
205 """Suspends the provided server."""
206 return self.action(server_id, 'suspend', None, **kwargs)
207
208 def resume_server(self, server_id, **kwargs):
209 """Un-suspends the provided server."""
210 return self.action(server_id, 'resume', None, **kwargs)
211
212 def pause_server(self, server_id, **kwargs):
213 """Pauses the provided server."""
214 return self.action(server_id, 'pause', None, **kwargs)
215
216 def unpause_server(self, server_id, **kwargs):
217 """Un-pauses the provided server."""
218 return self.action(server_id, 'unpause', None, **kwargs)
219
220 def reset_state(self, server_id, state='error'):
221 """Resets the state of a server to active/error."""
222 return self.action(server_id, 'os-resetState', None, state=state)
223
224 def delete_server(self, server_id):
225 """Deletes the given server."""
226 return self.delete("servers/%s" % str(server_id))
227
228 def _parse_array(self, node):
229 array = []
230 for child in node.getchildren():
231 array.append(xml_to_json(child))
232 return array
233
234 def list_servers(self, params=None):
235 url = 'servers'
236 if params:
237 url += '?%s' % urllib.urlencode(params)
238
239 resp, body = self.get(url, self.headers)
240 servers = self._parse_array(etree.fromstring(body))
241 return resp, {"servers": servers}
242
243 def list_servers_with_detail(self, params=None):
244 url = 'servers/detail'
245 if params:
246 url += '?%s' % urllib.urlencode(params)
247
248 resp, body = self.get(url, self.headers)
249 servers = self._parse_array(etree.fromstring(body))
250 return resp, {"servers": servers}
251
252 def update_server(self, server_id, name=None, meta=None, accessIPv4=None,
253 accessIPv6=None, disk_config=None):
254 doc = Document()
255 server = Element("server")
256 doc.append(server)
257
258 if name is not None:
259 server.add_attr("name", name)
260 if accessIPv4 is not None:
261 server.add_attr("accessIPv4", accessIPv4)
262 if accessIPv6 is not None:
263 server.add_attr("accessIPv6", accessIPv6)
264 if disk_config is not None:
265 server.add_attr('xmlns:OS-DCF', "http://docs.openstack.org/"
266 "compute/ext/disk_config/api/v1.1")
267 server.add_attr("OS-DCF:diskConfig", disk_config)
268 if meta is not None:
269 metadata = Element("metadata")
270 server.append(metadata)
271 for k, v in meta:
272 meta = Element("meta", key=k)
273 meta.append(Text(v))
274 metadata.append(meta)
275
276 resp, body = self.put('servers/%s' % str(server_id),
277 str(doc), self.headers)
278 return resp, xml_to_json(etree.fromstring(body))
279
280 def create_server(self, name, image_ref, flavor_ref, **kwargs):
281 """
282 Creates an instance of a server.
283 name (Required): The name of the server.
284 image_ref (Required): Reference to the image used to build the server.
285 flavor_ref (Required): The flavor used to build the server.
286 Following optional keyword arguments are accepted:
287 adminPass: Sets the initial root password.
288 key_name: Key name of keypair that was created earlier.
289 meta: A dictionary of values to be used as metadata.
290 personality: A list of dictionaries for files to be injected into
291 the server.
292 security_groups: A list of security group dicts.
293 networks: A list of network dicts with UUID and fixed_ip.
294 user_data: User data for instance.
295 availability_zone: Availability zone in which to launch instance.
296 accessIPv4: The IPv4 access address for the server.
297 accessIPv6: The IPv6 access address for the server.
298 min_count: Count of minimum number of instances to launch.
299 max_count: Count of maximum number of instances to launch.
300 disk_config: Determines if user or admin controls disk configuration.
301 """
302 server = Element("server",
303 xmlns=XMLNS_11,
304 imageRef=image_ref,
305 flavorRef=flavor_ref,
306 name=name)
307
308 for attr in ["adminPass", "accessIPv4", "accessIPv6", "key_name",
309 "user_data", "availability_zone", "min_count",
310 "max_count", "return_reservation_id"]:
311 if attr in kwargs:
312 server.add_attr(attr, kwargs[attr])
313
314 if 'disk_config' in kwargs:
315 server.add_attr('xmlns:OS-DCF', "http://docs.openstack.org/"
316 "compute/ext/disk_config/api/v1.1")
317 server.add_attr('OS-DCF:diskConfig', kwargs['disk_config'])
318
319 if 'security_groups' in kwargs:
320 secgroups = Element("security_groups")
321 server.append(secgroups)
322 for secgroup in kwargs['security_groups']:
323 s = Element("security_group", name=secgroup['name'])
324 secgroups.append(s)
325
326 if 'networks' in kwargs:
327 networks = Element("networks")
328 server.append(networks)
329 for network in kwargs['networks']:
330 s = Element("network", uuid=network['uuid'],
331 fixed_ip=network['fixed_ip'])
332 networks.append(s)
333
334 if 'meta' in kwargs:
335 metadata = Element("metadata")
336 server.append(metadata)
337 for k, v in kwargs['meta'].items():
338 meta = Element("meta", key=k)
339 meta.append(Text(v))
340 metadata.append(meta)
341
342 if 'personality' in kwargs:
343 personality = Element('personality')
344 server.append(personality)
345 for k in kwargs['personality']:
346 temp = Element('file', path=k['path'])
347 temp.append(Text(k['contents']))
348 personality.append(temp)
349
350 resp, body = self.post('servers', str(Document(server)), self.headers)
351 server = self._parse_server(etree.fromstring(body))
352 return resp, server
353
354 def wait_for_server_status(self, server_id, status):
355 """Waits for a server to reach a given status."""
356 return waiters.wait_for_server_status(self, server_id, status)
357
358 def wait_for_server_termination(self, server_id, ignore_error=False):
359 """Waits for server to reach termination."""
360 start_time = int(time.time())
361 while True:
362 try:
363 resp, body = self.get_server(server_id)
364 except exceptions.NotFound:
365 return
366
367 server_status = body['status']
368 if server_status == 'ERROR' and not ignore_error:
369 raise exceptions.BuildErrorException
370
371 if int(time.time()) - start_time >= self.build_timeout:
372 raise exceptions.TimeoutException
373
374 time.sleep(self.build_interval)
375
376 def _parse_network(self, node):
377 addrs = []
378 for child in node.getchildren():
379 addrs.append({'version': int(child.get('version')),
380 'addr': child.get('addr')})
381 return {node.get('id'): addrs}
382
383 def list_addresses(self, server_id):
384 """Lists all addresses for a server."""
385 resp, body = self.get("servers/%s/ips" % str(server_id), self.headers)
386
387 networks = {}
388 xml_list = etree.fromstring(body)
389 for child in xml_list.getchildren():
390 network = self._parse_network(child)
391 networks.update(**network)
392
393 return resp, networks
394
395 def list_addresses_by_network(self, server_id, network_id):
396 """Lists all addresses of a specific network type for a server."""
397 resp, body = self.get("servers/%s/ips/%s" % (str(server_id),
398 network_id),
399 self.headers)
400 network = self._parse_network(etree.fromstring(body))
401
402 return resp, network
403
404 def action(self, server_id, action_name, response_key, **kwargs):
405 if 'xmlns' not in kwargs:
406 kwargs['xmlns'] = XMLNS_11
407 doc = Document((Element(action_name, **kwargs)))
408 resp, body = self.post("servers/%s/action" % server_id,
409 str(doc), self.headers)
410 if response_key is not None:
411 body = xml_to_json(etree.fromstring(body))
412 return resp, body
413
414 def change_password(self, server_id, password):
415 return self.action(server_id, "changePassword", None,
416 adminPass=password)
417
418 def reboot(self, server_id, reboot_type):
419 return self.action(server_id, "reboot", None, type=reboot_type)
420
421 def rebuild(self, server_id, image_ref, **kwargs):
422 kwargs['imageRef'] = image_ref
423 if 'disk_config' in kwargs:
424 kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
425 del kwargs['disk_config']
426 kwargs['xmlns:OS-DCF'] = "http://docs.openstack.org/"\
427 "compute/ext/disk_config/api/v1.1"
428 kwargs['xmlns:atom'] = "http://www.w3.org/2005/Atom"
429 if 'xmlns' not in kwargs:
430 kwargs['xmlns'] = XMLNS_11
431
432 attrs = kwargs.copy()
433 if 'metadata' in attrs:
434 del attrs['metadata']
435 rebuild = Element("rebuild",
436 **attrs)
437
438 if 'metadata' in kwargs:
439 metadata = Element("metadata")
440 rebuild.append(metadata)
441 for k, v in kwargs['metadata'].items():
442 meta = Element("meta", key=k)
443 meta.append(Text(v))
444 metadata.append(meta)
445
446 resp, body = self.post('servers/%s/action' % server_id,
447 str(Document(rebuild)), self.headers)
448 server = self._parse_server(etree.fromstring(body))
449 return resp, server
450
451 def resize(self, server_id, flavor_ref, **kwargs):
452 if 'disk_config' in kwargs:
453 kwargs['OS-DCF:diskConfig'] = kwargs['disk_config']
454 del kwargs['disk_config']
455 kwargs['xmlns:OS-DCF'] = "http://docs.openstack.org/"\
456 "compute/ext/disk_config/api/v1.1"
457 kwargs['xmlns:atom'] = "http://www.w3.org/2005/Atom"
458 kwargs['flavorRef'] = flavor_ref
459 return self.action(server_id, 'resize', None, **kwargs)
460
461 def confirm_resize(self, server_id, **kwargs):
462 return self.action(server_id, 'confirmResize', None, **kwargs)
463
464 def revert_resize(self, server_id, **kwargs):
465 return self.action(server_id, 'revertResize', None, **kwargs)
466
467 def stop(self, server_id, **kwargs):
468 return self.action(server_id, 'os-stop', None, **kwargs)
469
470 def start(self, server_id, **kwargs):
471 return self.action(server_id, 'os-start', None, **kwargs)
472
473 def create_image(self, server_id, name):
474 return self.action(server_id, 'createImage', None, name=name)
475
476 def add_security_group(self, server_id, name):
477 return self.action(server_id, 'addSecurityGroup', None, name=name)
478
479 def remove_security_group(self, server_id, name):
480 return self.action(server_id, 'removeSecurityGroup', None, name=name)
481
482 def live_migrate_server(self, server_id, dest_host, use_block_migration):
483 """This should be called with administrator privileges ."""
484
485 req_body = Element("os-migrateLive",
486 xmlns=XMLNS_11,
487 disk_over_commit=False,
488 block_migration=use_block_migration,
489 host=dest_host)
490
491 resp, body = self.post("servers/%s/action" % str(server_id),
492 str(Document(req_body)), self.headers)
493 return resp, body
494
495 def list_server_metadata(self, server_id):
496 resp, body = self.get("servers/%s/metadata" % str(server_id),
497 self.headers)
498 body = self._parse_key_value(etree.fromstring(body))
499 return resp, body
500
501 def set_server_metadata(self, server_id, meta, no_metadata_field=False):
502 doc = Document()
503 if not no_metadata_field:
504 metadata = Element("metadata")
505 doc.append(metadata)
506 for k, v in meta.items():
507 meta_element = Element("meta", key=k)
508 meta_element.append(Text(v))
509 metadata.append(meta_element)
510 resp, body = self.put('servers/%s/metadata' % str(server_id),
511 str(doc), self.headers)
512 return resp, xml_to_json(etree.fromstring(body))
513
514 def update_server_metadata(self, server_id, meta):
515 doc = Document()
516 metadata = Element("metadata")
517 doc.append(metadata)
518 for k, v in meta.items():
519 meta_element = Element("meta", key=k)
520 meta_element.append(Text(v))
521 metadata.append(meta_element)
522 resp, body = self.post("/servers/%s/metadata" % str(server_id),
523 str(doc), headers=self.headers)
524 body = xml_to_json(etree.fromstring(body))
525 return resp, body
526
527 def get_server_metadata_item(self, server_id, key):
528 resp, body = self.get("servers/%s/metadata/%s" % (str(server_id), key),
529 headers=self.headers)
530 return resp, dict([(etree.fromstring(body).attrib['key'],
531 xml_to_json(etree.fromstring(body)))])
532
533 def set_server_metadata_item(self, server_id, key, meta):
534 doc = Document()
535 for k, v in meta.items():
536 meta_element = Element("meta", key=k)
537 meta_element.append(Text(v))
538 doc.append(meta_element)
539 resp, body = self.put('servers/%s/metadata/%s' % (str(server_id), key),
540 str(doc), self.headers)
541 return resp, xml_to_json(etree.fromstring(body))
542
543 def delete_server_metadata_item(self, server_id, key):
544 resp, body = self.delete("servers/%s/metadata/%s" %
545 (str(server_id), key))
546 return resp, body
547
548 def get_console_output(self, server_id, length):
549 return self.action(server_id, 'os-getConsoleOutput', 'output',
550 length=length)
551
552 def list_virtual_interfaces(self, server_id):
553 """
554 List the virtual interfaces used in an instance.
555 """
556 resp, body = self.get('/'.join(['servers', server_id,
557 'os-virtual-interfaces']), self.headers)
558 virt_int = self._parse_xml_virtual_interfaces(etree.fromstring(body))
559 return resp, virt_int
560
561 def rescue_server(self, server_id, adminPass=None):
562 """Rescue the provided server."""
563 return self.action(server_id, 'rescue', None, adminPass=adminPass)
564
565 def unrescue_server(self, server_id):
566 """Unrescue the provided server."""
567 return self.action(server_id, 'unrescue', None)
568
569 def attach_volume(self, server_id, volume_id, device='/dev/vdz'):
570 post_body = Element("volumeAttachment", volumeId=volume_id,
571 device=device)
572 resp, body = self.post('servers/%s/os-volume_attachments' % server_id,
573 str(Document(post_body)), self.headers)
574 return resp, body
575
576 def detach_volume(self, server_id, volume_id):
577 headers = {'Content-Type': 'application/xml',
578 'Accept': 'application/xml'}
579 resp, body = self.delete('servers/%s/os-volume_attachments/%s' %
580 (server_id, volume_id), headers)
581 return resp, body
582
583 def get_server_diagnostics(self, server_id):
584 """Get the usage data for a server."""
585 resp, body = self.get("servers/%s/diagnostics" % server_id,
586 self.headers)
587 body = xml_to_json(etree.fromstring(body))
588 return resp, body
589
590 def list_instance_actions(self, server_id):
591 """List the provided server action."""
592 resp, body = self.get("servers/%s/os-instance-actions" % server_id,
593 self.headers)
594 body = self._parse_array(etree.fromstring(body))
595 return resp, body
596
597 def get_instance_action(self, server_id, request_id):
598 """Returns the action details of the provided server."""
599 resp, body = self.get("servers/%s/os-instance-actions/%s" %
600 (server_id, request_id), self.headers)
601 body = xml_to_json(etree.fromstring(body))
602 return resp, body