Adds ability to boot a server via the Nova V3 API
Creates an images client when attached to the the servers client. This is necessary because the Nova V3 API no longer proxies image queries to glance but when preparing a request to boot a server it is necessary to retreive information about images so we need to talk to both Nova and Glance in the same command. This is a bit ugly, but not much more than the already existing ugliness of using the client class designed to talk to Nova to talk to Glance and Cinder. The long term clean solution is probably to a unified client that is designed to talk to multiple openstack services. Differences between the V2 and V3 API are described here: https://wiki.openstack.org/wiki/NovaAPIv2tov3 Partially implements blueprint v3-api Change-Id: Ib43682f38cd7a3e0f910b75e96685591246e7f67
This commit is contained in:
parent
eb0b6d167d
commit
c8ad315763
@ -20,7 +20,6 @@ Base utilities to build API operation managers and objects on top of.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
import base64
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import hashlib
|
import hashlib
|
||||||
import inspect
|
import inspect
|
||||||
@ -31,7 +30,6 @@ import six
|
|||||||
|
|
||||||
from novaclient import exceptions
|
from novaclient import exceptions
|
||||||
from novaclient.openstack.common.apiclient import base
|
from novaclient.openstack.common.apiclient import base
|
||||||
from novaclient.openstack.common import strutils
|
|
||||||
from novaclient import utils
|
from novaclient import utils
|
||||||
|
|
||||||
Resource = base.Resource
|
Resource = base.Resource
|
||||||
@ -276,140 +274,3 @@ class BootingManagerWithFind(ManagerWithFind):
|
|||||||
|
|
||||||
bdm.append(bdm_dict)
|
bdm.append(bdm_dict)
|
||||||
return bdm
|
return bdm
|
||||||
|
|
||||||
def _boot(self, resource_url, response_key, name, image, flavor,
|
|
||||||
meta=None, files=None, userdata=None,
|
|
||||||
reservation_id=None, return_raw=False, min_count=None,
|
|
||||||
max_count=None, security_groups=None, key_name=None,
|
|
||||||
availability_zone=None, block_device_mapping=None,
|
|
||||||
block_device_mapping_v2=None, nics=None, scheduler_hints=None,
|
|
||||||
config_drive=None, admin_pass=None, disk_config=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Create (boot) a new server.
|
|
||||||
|
|
||||||
:param name: Something to name the server.
|
|
||||||
:param image: The :class:`Image` to boot with.
|
|
||||||
:param flavor: The :class:`Flavor` to boot onto.
|
|
||||||
:param meta: A dict of arbitrary key/value metadata to store for this
|
|
||||||
server. A maximum of five entries is allowed, and both
|
|
||||||
keys and values must be 255 characters or less.
|
|
||||||
:param files: A dict of files to overwrite on the server upon boot.
|
|
||||||
Keys are file names (i.e. ``/etc/passwd``) and values
|
|
||||||
are the file contents (either as a string or as a
|
|
||||||
file-like object). A maximum of five entries is allowed,
|
|
||||||
and each file must be 10k or less.
|
|
||||||
:param reservation_id: a UUID for the set of servers being requested.
|
|
||||||
:param return_raw: If True, don't try to coearse the result into
|
|
||||||
a Resource object.
|
|
||||||
:param security_groups: list of security group names
|
|
||||||
:param key_name: (optional extension) name of keypair to inject into
|
|
||||||
the instance
|
|
||||||
:param availability_zone: Name of the availability zone for instance
|
|
||||||
placement.
|
|
||||||
:param block_device_mapping: A dict of block device mappings for this
|
|
||||||
server.
|
|
||||||
:param block_device_mapping_v2: A dict of block device mappings V2 for
|
|
||||||
this server.
|
|
||||||
:param nics: (optional extension) an ordered list of nics to be
|
|
||||||
added to this server, with information about
|
|
||||||
connected networks, fixed ips, etc.
|
|
||||||
:param scheduler_hints: (optional extension) arbitrary key-value pairs
|
|
||||||
specified by the client to help boot an instance.
|
|
||||||
:param config_drive: (optional extension) value for config drive
|
|
||||||
either boolean, or volume-id
|
|
||||||
:param admin_pass: admin password for the server.
|
|
||||||
:param disk_config: (optional extension) control how the disk is
|
|
||||||
partitioned when the server is created.
|
|
||||||
"""
|
|
||||||
body = {"server": {
|
|
||||||
"name": name,
|
|
||||||
"imageRef": str(getid(image)) if image else '',
|
|
||||||
"flavorRef": str(getid(flavor)),
|
|
||||||
}}
|
|
||||||
if userdata:
|
|
||||||
if hasattr(userdata, 'read'):
|
|
||||||
userdata = userdata.read()
|
|
||||||
|
|
||||||
if six.PY3:
|
|
||||||
userdata = userdata.encode("utf-8")
|
|
||||||
else:
|
|
||||||
userdata = strutils.safe_encode(userdata)
|
|
||||||
|
|
||||||
body["server"]["user_data"] = base64.b64encode(userdata)
|
|
||||||
if meta:
|
|
||||||
body["server"]["metadata"] = meta
|
|
||||||
if reservation_id:
|
|
||||||
body["server"]["reservation_id"] = reservation_id
|
|
||||||
if key_name:
|
|
||||||
body["server"]["key_name"] = key_name
|
|
||||||
if scheduler_hints:
|
|
||||||
body['os:scheduler_hints'] = scheduler_hints
|
|
||||||
if config_drive:
|
|
||||||
body["server"]["config_drive"] = config_drive
|
|
||||||
if admin_pass:
|
|
||||||
body["server"]["adminPass"] = admin_pass
|
|
||||||
if not min_count:
|
|
||||||
min_count = 1
|
|
||||||
if not max_count:
|
|
||||||
max_count = min_count
|
|
||||||
body["server"]["min_count"] = min_count
|
|
||||||
body["server"]["max_count"] = max_count
|
|
||||||
|
|
||||||
if security_groups:
|
|
||||||
body["server"]["security_groups"] =\
|
|
||||||
[{'name': sg} for sg in security_groups]
|
|
||||||
|
|
||||||
# Files are a slight bit tricky. They're passed in a "personality"
|
|
||||||
# list to the POST. Each item is a dict giving a file name and the
|
|
||||||
# base64-encoded contents of the file. We want to allow passing
|
|
||||||
# either an open file *or* some contents as files here.
|
|
||||||
if files:
|
|
||||||
personality = body['server']['personality'] = []
|
|
||||||
for filepath, file_or_string in sorted(files.items(),
|
|
||||||
key=lambda x: x[0]):
|
|
||||||
if hasattr(file_or_string, 'read'):
|
|
||||||
data = file_or_string.read()
|
|
||||||
else:
|
|
||||||
data = file_or_string
|
|
||||||
personality.append({
|
|
||||||
'path': filepath,
|
|
||||||
'contents': base64.b64encode(data.encode('utf-8')),
|
|
||||||
})
|
|
||||||
|
|
||||||
if availability_zone:
|
|
||||||
body["server"]["availability_zone"] = availability_zone
|
|
||||||
|
|
||||||
# Block device mappings are passed as a list of dictionaries
|
|
||||||
if block_device_mapping:
|
|
||||||
body['server']['block_device_mapping'] = \
|
|
||||||
self._parse_block_device_mapping(block_device_mapping)
|
|
||||||
elif block_device_mapping_v2:
|
|
||||||
# Append the image to the list only if we have new style BDMs
|
|
||||||
if image:
|
|
||||||
bdm_dict = {'uuid': image.id, 'source_type': 'image',
|
|
||||||
'destination_type': 'local', 'boot_index': 0,
|
|
||||||
'delete_on_termination': True}
|
|
||||||
block_device_mapping_v2.insert(0, bdm_dict)
|
|
||||||
|
|
||||||
body['server']['block_device_mapping_v2'] = block_device_mapping_v2
|
|
||||||
|
|
||||||
if nics is not None:
|
|
||||||
# NOTE(tr3buchet): nics can be an empty list
|
|
||||||
all_net_data = []
|
|
||||||
for nic_info in nics:
|
|
||||||
net_data = {}
|
|
||||||
# if value is empty string, do not send value in body
|
|
||||||
if nic_info.get('net-id'):
|
|
||||||
net_data['uuid'] = nic_info['net-id']
|
|
||||||
if nic_info.get('v4-fixed-ip'):
|
|
||||||
net_data['fixed_ip'] = nic_info['v4-fixed-ip']
|
|
||||||
if nic_info.get('port-id'):
|
|
||||||
net_data['port'] = nic_info['port-id']
|
|
||||||
all_net_data.append(net_data)
|
|
||||||
body['server']['networks'] = all_net_data
|
|
||||||
|
|
||||||
if disk_config is not None:
|
|
||||||
body['server']['OS-DCF:diskConfig'] = disk_config
|
|
||||||
|
|
||||||
return self._create(resource_url, body, response_key,
|
|
||||||
return_raw=return_raw, **kwargs)
|
|
||||||
|
@ -686,6 +686,25 @@ class OpenStackComputeShell(object):
|
|||||||
except exc.AuthorizationFailure:
|
except exc.AuthorizationFailure:
|
||||||
raise exc.CommandError("Unable to authorize user")
|
raise exc.CommandError("Unable to authorize user")
|
||||||
|
|
||||||
|
if os_compute_api_version == "3" and service_type != 'image':
|
||||||
|
# NOTE(cyeoh): create an image based client because the
|
||||||
|
# images api is no longer proxied by the V3 API and we
|
||||||
|
# sometimes need to be able to look up images information
|
||||||
|
# via glance when connected to the nova api.
|
||||||
|
image_service_type = 'image'
|
||||||
|
self.cs.image_cs = client.Client(
|
||||||
|
options.os_compute_api_version, os_username,
|
||||||
|
os_password, os_tenant_name, tenant_id=os_tenant_id,
|
||||||
|
auth_url=os_auth_url, insecure=insecure,
|
||||||
|
region_name=os_region_name, endpoint_type=endpoint_type,
|
||||||
|
extensions=self.extensions, service_type=image_service_type,
|
||||||
|
service_name=service_name, auth_system=os_auth_system,
|
||||||
|
auth_plugin=auth_plugin,
|
||||||
|
volume_service_name=volume_service_name,
|
||||||
|
timings=args.timings, bypass_url=bypass_url,
|
||||||
|
os_cache=os_cache, http_log_debug=options.debug,
|
||||||
|
cacert=cacert, timeout=timeout)
|
||||||
|
|
||||||
args.func(self.cs, args)
|
args.func(self.cs, args)
|
||||||
|
|
||||||
if args.timings:
|
if args.timings:
|
||||||
|
@ -222,7 +222,7 @@ class ShellTest(utils.TestCase):
|
|||||||
cmd = '--os-compute-api-version %s list' % version
|
cmd = '--os-compute-api-version %s list' % version
|
||||||
self.make_env()
|
self.make_env()
|
||||||
self.shell(cmd)
|
self.shell(cmd)
|
||||||
_, client_kwargs = mock_client.call_args
|
_, client_kwargs = mock_client.call_args_list[0]
|
||||||
self.assertEqual(service_type, client_kwargs['service_type'])
|
self.assertEqual(service_type, client_kwargs['service_type'])
|
||||||
|
|
||||||
@mock.patch('novaclient.client.Client')
|
@mock.patch('novaclient.client.Client')
|
||||||
|
@ -157,6 +157,14 @@ class FakeHTTPClient(fakes_v1_1.FakeHTTPClient):
|
|||||||
def post_servers_1234_os_attach_interfaces(self, **kw):
|
def post_servers_1234_os_attach_interfaces(self, **kw):
|
||||||
return (200, {}, {'interface_attachment': {}})
|
return (200, {}, {'interface_attachment': {}})
|
||||||
|
|
||||||
|
def post_servers(self, body, **kw):
|
||||||
|
assert set(body.keys()) <= set(['server'])
|
||||||
|
fakes.assert_has_keys(body['server'],
|
||||||
|
required=['name', 'image_ref', 'flavor_ref'],
|
||||||
|
optional=['metadata', 'personality',
|
||||||
|
'os-scheduler-hints:scheduler_hints'])
|
||||||
|
return (202, {}, self.get_servers_1234()[2])
|
||||||
|
|
||||||
#
|
#
|
||||||
# Server Actions
|
# Server Actions
|
||||||
#
|
#
|
||||||
|
@ -178,11 +178,11 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -192,11 +192,11 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 3,
|
'os-multiple-create:max_count': 3,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -206,11 +206,11 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -219,12 +219,12 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'key_name': '1',
|
'key_name': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -243,12 +243,13 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
'user_data': base64.b64encode(file_text.encode('utf-8'))
|
'os-user-data:user_data': base64.b64encode(
|
||||||
|
file_text.encode('utf-8'))
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -259,12 +260,12 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'availability_zone': 'avzone',
|
'os-availability-zone:availability_zone': 'avzone',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1
|
'os-multiple-create:max_count': 1
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -275,13 +276,13 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'security_groups': [{'name': 'secgroup1'},
|
'os-security-groups:security_groups': [{'name': 'secgroup1'},
|
||||||
{'name': 'secgroup2'}],
|
{'name': 'secgroup2'}],
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -291,12 +292,12 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
'config_drive': True
|
'os-config-drive:config_drive': True
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -306,12 +307,12 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
'config_drive': '/dev/hda'
|
'os-config-drive:config_drive': '/dev/hda'
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -335,20 +336,20 @@ class ShellTest(utils.TestCase):
|
|||||||
'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server'
|
'boot --flavor 1 --block_device_mapping vda=blah:::0 some-server'
|
||||||
)
|
)
|
||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/os-volumes_boot',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'block_device_mapping': [
|
'os-block-device-mapping:block_device_mapping': [
|
||||||
{
|
{
|
||||||
'volume_id': 'blah',
|
'volume_id': 'blah',
|
||||||
'delete_on_termination': '0',
|
'delete_on_termination': '0',
|
||||||
'device_name': 'vda'
|
'device_name': 'vda'
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
'imageRef': '',
|
'image_ref': '',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -359,17 +360,17 @@ class ShellTest(utils.TestCase):
|
|||||||
'type=disk,shutdown=preserve some-server'
|
'type=disk,shutdown=preserve some-server'
|
||||||
)
|
)
|
||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/os-volumes_boot',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'block_device_mapping': [
|
'os-block-device-mapping:block_device_mapping': [
|
||||||
{'device_name': 'id', 'volume_id':
|
{'device_name': 'id', 'volume_id':
|
||||||
'fake-id,source=volume,dest=volume,device=vda,size=1,'
|
'fake-id,source=volume,dest=volume,device=vda,size=1,'
|
||||||
'format=ext4,type=disk,shutdown=preserve'}],
|
'format=ext4,type=disk,shutdown=preserve'}],
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -379,12 +380,12 @@ class ShellTest(utils.TestCase):
|
|||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{'server': {
|
{'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'metadata': {'foo': 'bar=pants', 'spam': 'eggs'},
|
'metadata': {'foo': 'bar=pants', 'spam': 'eggs'},
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
}},
|
}},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -396,13 +397,14 @@ class ShellTest(utils.TestCase):
|
|||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{
|
{
|
||||||
'server': {
|
'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
|
'os-scheduler-hints:scheduler_hints': {
|
||||||
|
'a': ['b1=c1', 'b0=c0'], 'a2': 'b2=c2'},
|
||||||
},
|
},
|
||||||
'os:scheduler_hints': {'a': ['b1=c1', 'b0=c0'], 'a2': 'b2=c2'},
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -414,11 +416,11 @@ class ShellTest(utils.TestCase):
|
|||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{
|
{
|
||||||
'server': {
|
'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'some-server',
|
'name': 'some-server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 1,
|
'os-multiple-create:max_count': 1,
|
||||||
'networks': [
|
'networks': [
|
||||||
{'uuid': 'a=c', 'fixed_ip': '10.0.0.1'},
|
{'uuid': 'a=c', 'fixed_ip': '10.0.0.1'},
|
||||||
],
|
],
|
||||||
@ -441,57 +443,17 @@ class ShellTest(utils.TestCase):
|
|||||||
'--nic v4-fixed-ip=10.0.0.1 some-server')
|
'--nic v4-fixed-ip=10.0.0.1 some-server')
|
||||||
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
|
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
|
||||||
|
|
||||||
def test_boot_files(self):
|
|
||||||
file_text = 'text'
|
|
||||||
|
|
||||||
with mock.patch('novaclient.v3.shell.open', create=True) as mock_open:
|
|
||||||
mock_open.return_value = file_text
|
|
||||||
testfile = 'some_dir/some_file.txt'
|
|
||||||
|
|
||||||
self.run_command('boot --flavor 1 --image 1 --user_data %s '
|
|
||||||
'some-server' % testfile)
|
|
||||||
|
|
||||||
cmd = ('boot some-server --flavor 1 --image 1'
|
|
||||||
' --file /tmp/foo=%s --file /tmp/bar=%s')
|
|
||||||
self.run_command(cmd % (testfile, testfile))
|
|
||||||
|
|
||||||
mock_open.assert_called_with(testfile)
|
|
||||||
|
|
||||||
self.assert_called_anytime(
|
|
||||||
'POST', '/servers',
|
|
||||||
{'server': {
|
|
||||||
'flavorRef': '1',
|
|
||||||
'name': 'some-server',
|
|
||||||
'imageRef': '1',
|
|
||||||
'min_count': 1,
|
|
||||||
'max_count': 1,
|
|
||||||
'personality': [
|
|
||||||
{'path': '/tmp/bar',
|
|
||||||
'contents': base64.b64encode(file_text.encode('utf-8'))},
|
|
||||||
{'path': '/tmp/foo',
|
|
||||||
'contents': base64.b64encode(file_text.encode('utf-8'))},
|
|
||||||
]
|
|
||||||
}},
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_boot_invalid_files(self):
|
|
||||||
invalid_file = os.path.join(os.path.dirname(__file__),
|
|
||||||
'asdfasdfasdfasdf')
|
|
||||||
cmd = ('boot some-server --flavor 1 --image 1'
|
|
||||||
' --file /foo=%s' % invalid_file)
|
|
||||||
self.assertRaises(exceptions.CommandError, self.run_command, cmd)
|
|
||||||
|
|
||||||
def test_boot_num_instances(self):
|
def test_boot_num_instances(self):
|
||||||
self.run_command('boot --image 1 --flavor 1 --num-instances 3 server')
|
self.run_command('boot --image 1 --flavor 1 --num-instances 3 server')
|
||||||
self.assert_called_anytime(
|
self.assert_called_anytime(
|
||||||
'POST', '/servers',
|
'POST', '/servers',
|
||||||
{
|
{
|
||||||
'server': {
|
'server': {
|
||||||
'flavorRef': '1',
|
'flavor_ref': '1',
|
||||||
'name': 'server',
|
'name': 'server',
|
||||||
'imageRef': '1',
|
'image_ref': '1',
|
||||||
'min_count': 1,
|
'os-multiple-create:min_count': 1,
|
||||||
'max_count': 3,
|
'os-multiple-create:max_count': 3,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -19,11 +19,14 @@
|
|||||||
Server interface.
|
Server interface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from novaclient import base
|
from novaclient import base
|
||||||
from novaclient import crypto
|
from novaclient import crypto
|
||||||
from novaclient.openstack.common.py3kcompat import urlutils
|
from novaclient.openstack.common.py3kcompat import urlutils
|
||||||
|
from novaclient.openstack.common import strutils
|
||||||
from novaclient.v1_1.security_groups import SecurityGroup
|
from novaclient.v1_1.security_groups import SecurityGroup
|
||||||
|
|
||||||
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
||||||
@ -385,6 +388,143 @@ class Server(base.Resource):
|
|||||||
class ServerManager(base.BootingManagerWithFind):
|
class ServerManager(base.BootingManagerWithFind):
|
||||||
resource_class = Server
|
resource_class = Server
|
||||||
|
|
||||||
|
def _boot(self, resource_url, response_key, name, image, flavor,
|
||||||
|
meta=None, files=None, userdata=None,
|
||||||
|
reservation_id=None, return_raw=False, min_count=None,
|
||||||
|
max_count=None, security_groups=None, key_name=None,
|
||||||
|
availability_zone=None, block_device_mapping=None,
|
||||||
|
block_device_mapping_v2=None, nics=None, scheduler_hints=None,
|
||||||
|
config_drive=None, admin_pass=None, disk_config=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Create (boot) a new server.
|
||||||
|
|
||||||
|
:param name: Something to name the server.
|
||||||
|
:param image: The :class:`Image` to boot with.
|
||||||
|
:param flavor: The :class:`Flavor` to boot onto.
|
||||||
|
:param meta: A dict of arbitrary key/value metadata to store for this
|
||||||
|
server. A maximum of five entries is allowed, and both
|
||||||
|
keys and values must be 255 characters or less.
|
||||||
|
:param files: A dict of files to overwrite on the server upon boot.
|
||||||
|
Keys are file names (i.e. ``/etc/passwd``) and values
|
||||||
|
are the file contents (either as a string or as a
|
||||||
|
file-like object). A maximum of five entries is allowed,
|
||||||
|
and each file must be 10k or less.
|
||||||
|
:param reservation_id: a UUID for the set of servers being requested.
|
||||||
|
:param return_raw: If True, don't try to coearse the result into
|
||||||
|
a Resource object.
|
||||||
|
:param security_groups: list of security group names
|
||||||
|
:param key_name: (optional extension) name of keypair to inject into
|
||||||
|
the instance
|
||||||
|
:param availability_zone: Name of the availability zone for instance
|
||||||
|
placement.
|
||||||
|
:param block_device_mapping: A dict of block device mappings for this
|
||||||
|
server.
|
||||||
|
:param block_device_mapping_v2: A dict of block device mappings V2 for
|
||||||
|
this server.
|
||||||
|
:param nics: (optional extension) an ordered list of nics to be
|
||||||
|
added to this server, with information about
|
||||||
|
connected networks, fixed ips, etc.
|
||||||
|
:param scheduler_hints: (optional extension) arbitrary key-value pairs
|
||||||
|
specified by the client to help boot an instance.
|
||||||
|
:param config_drive: (optional extension) value for config drive
|
||||||
|
either boolean, or volume-id
|
||||||
|
:param admin_pass: admin password for the server.
|
||||||
|
:param disk_config: (optional extension) control how the disk is
|
||||||
|
partitioned when the server is created.
|
||||||
|
"""
|
||||||
|
body = {"server": {
|
||||||
|
"name": name,
|
||||||
|
"imageRef": str(base.getid(image)) if image else '',
|
||||||
|
"flavorRef": str(base.getid(flavor)),
|
||||||
|
}}
|
||||||
|
if userdata:
|
||||||
|
if hasattr(userdata, 'read'):
|
||||||
|
userdata = userdata.read()
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
userdata = userdata.encode("utf-8")
|
||||||
|
else:
|
||||||
|
userdata = strutils.safe_encode(userdata)
|
||||||
|
|
||||||
|
body["server"]["user_data"] = base64.b64encode(userdata)
|
||||||
|
if meta:
|
||||||
|
body["server"]["metadata"] = meta
|
||||||
|
if reservation_id:
|
||||||
|
body["server"]["reservation_id"] = reservation_id
|
||||||
|
if key_name:
|
||||||
|
body["server"]["key_name"] = key_name
|
||||||
|
if scheduler_hints:
|
||||||
|
body['os:scheduler_hints'] = scheduler_hints
|
||||||
|
if config_drive:
|
||||||
|
body["server"]["config_drive"] = config_drive
|
||||||
|
if admin_pass:
|
||||||
|
body["server"]["adminPass"] = admin_pass
|
||||||
|
if not min_count:
|
||||||
|
min_count = 1
|
||||||
|
if not max_count:
|
||||||
|
max_count = min_count
|
||||||
|
body["server"]["min_count"] = min_count
|
||||||
|
body["server"]["max_count"] = max_count
|
||||||
|
|
||||||
|
if security_groups:
|
||||||
|
body["server"]["security_groups"] =\
|
||||||
|
[{'name': sg} for sg in security_groups]
|
||||||
|
|
||||||
|
# Files are a slight bit tricky. They're passed in a "personality"
|
||||||
|
# list to the POST. Each item is a dict giving a file name and the
|
||||||
|
# base64-encoded contents of the file. We want to allow passing
|
||||||
|
# either an open file *or* some contents as files here.
|
||||||
|
if files:
|
||||||
|
personality = body['server']['personality'] = []
|
||||||
|
for filepath, file_or_string in sorted(files.items(),
|
||||||
|
key=lambda x: x[0]):
|
||||||
|
if hasattr(file_or_string, 'read'):
|
||||||
|
data = file_or_string.read()
|
||||||
|
else:
|
||||||
|
data = file_or_string
|
||||||
|
personality.append({
|
||||||
|
'path': filepath,
|
||||||
|
'contents': base64.b64encode(data.encode('utf-8')),
|
||||||
|
})
|
||||||
|
|
||||||
|
if availability_zone:
|
||||||
|
body["server"]["availability_zone"] = availability_zone
|
||||||
|
|
||||||
|
# Block device mappings are passed as a list of dictionaries
|
||||||
|
if block_device_mapping:
|
||||||
|
body['server']['block_device_mapping'] = \
|
||||||
|
self._parse_block_device_mapping(block_device_mapping)
|
||||||
|
elif block_device_mapping_v2:
|
||||||
|
# Append the image to the list only if we have new style BDMs
|
||||||
|
if image:
|
||||||
|
bdm_dict = {'uuid': image.id, 'source_type': 'image',
|
||||||
|
'destination_type': 'local', 'boot_index': 0,
|
||||||
|
'delete_on_termination': True}
|
||||||
|
block_device_mapping_v2.insert(0, bdm_dict)
|
||||||
|
|
||||||
|
body['server']['block_device_mapping_v2'] = block_device_mapping_v2
|
||||||
|
|
||||||
|
if nics is not None:
|
||||||
|
# NOTE(tr3buchet): nics can be an empty list
|
||||||
|
all_net_data = []
|
||||||
|
for nic_info in nics:
|
||||||
|
net_data = {}
|
||||||
|
# if value is empty string, do not send value in body
|
||||||
|
if nic_info.get('net-id'):
|
||||||
|
net_data['uuid'] = nic_info['net-id']
|
||||||
|
if nic_info.get('v4-fixed-ip'):
|
||||||
|
net_data['fixed_ip'] = nic_info['v4-fixed-ip']
|
||||||
|
if nic_info.get('port-id'):
|
||||||
|
net_data['port'] = nic_info['port-id']
|
||||||
|
all_net_data.append(net_data)
|
||||||
|
body['server']['networks'] = all_net_data
|
||||||
|
|
||||||
|
if disk_config is not None:
|
||||||
|
body['server']['OS-DCF:diskConfig'] = disk_config
|
||||||
|
|
||||||
|
return self._create(resource_url, body, response_key,
|
||||||
|
return_raw=return_raw, **kwargs)
|
||||||
|
|
||||||
def get(self, server):
|
def get(self, server):
|
||||||
"""
|
"""
|
||||||
Get a server.
|
Get a server.
|
||||||
|
@ -19,11 +19,14 @@
|
|||||||
Server interface.
|
Server interface.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
import six
|
import six
|
||||||
|
|
||||||
from novaclient import base
|
from novaclient import base
|
||||||
from novaclient import crypto
|
from novaclient import crypto
|
||||||
from novaclient.openstack.common.py3kcompat import urlutils
|
from novaclient.openstack.common.py3kcompat import urlutils
|
||||||
|
from novaclient.openstack.common import strutils
|
||||||
|
|
||||||
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
REBOOT_SOFT, REBOOT_HARD = 'SOFT', 'HARD'
|
||||||
|
|
||||||
@ -349,6 +352,121 @@ class Server(base.Resource):
|
|||||||
class ServerManager(base.BootingManagerWithFind):
|
class ServerManager(base.BootingManagerWithFind):
|
||||||
resource_class = Server
|
resource_class = Server
|
||||||
|
|
||||||
|
def _boot(self, resource_url, response_key, name, image, flavor,
|
||||||
|
meta=None, userdata=None,
|
||||||
|
reservation_id=None, return_raw=False, min_count=None,
|
||||||
|
max_count=None, security_groups=None, key_name=None,
|
||||||
|
availability_zone=None, block_device_mapping=None,
|
||||||
|
block_device_mapping_v2=None, nics=None, scheduler_hints=None,
|
||||||
|
config_drive=None, admin_pass=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Create (boot) a new server.
|
||||||
|
|
||||||
|
:param name: Something to name the server.
|
||||||
|
:param image: The :class:`Image` to boot with.
|
||||||
|
:param flavor: The :class:`Flavor` to boot onto.
|
||||||
|
:param meta: A dict of arbitrary key/value metadata to store for this
|
||||||
|
server. A maximum of five entries is allowed, and both
|
||||||
|
keys and values must be 255 characters or less.
|
||||||
|
:param reservation_id: a UUID for the set of servers being requested.
|
||||||
|
:param return_raw: If True, don't try to coearse the result into
|
||||||
|
a Resource object.
|
||||||
|
:param security_groups: list of security group names
|
||||||
|
:param key_name: (optional extension) name of keypair to inject into
|
||||||
|
the instance
|
||||||
|
:param availability_zone: Name of the availability zone for instance
|
||||||
|
placement.
|
||||||
|
:param block_device_mapping: A dict of block device mappings for this
|
||||||
|
server.
|
||||||
|
:param block_device_mapping_v2: A dict of block device mappings V2 for
|
||||||
|
this server.
|
||||||
|
:param nics: (optional extension) an ordered list of nics to be
|
||||||
|
added to this server, with information about
|
||||||
|
connected networks, fixed ips, etc.
|
||||||
|
:param scheduler_hints: (optional extension) arbitrary key-value pairs
|
||||||
|
specified by the client to help boot an instance.
|
||||||
|
:param config_drive: (optional extension) value for config drive
|
||||||
|
either boolean, or volume-id
|
||||||
|
:param admin_pass: admin password for the server.
|
||||||
|
"""
|
||||||
|
body = {"server": {
|
||||||
|
"name": name,
|
||||||
|
"image_ref": str(base.getid(image)) if image else '',
|
||||||
|
"flavor_ref": str(base.getid(flavor)),
|
||||||
|
}}
|
||||||
|
if userdata:
|
||||||
|
if hasattr(userdata, 'read'):
|
||||||
|
userdata = userdata.read()
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
userdata = userdata.encode("utf-8")
|
||||||
|
else:
|
||||||
|
userdata = strutils.safe_encode(userdata)
|
||||||
|
|
||||||
|
body["server"][
|
||||||
|
"os-user-data:user_data"] = base64.b64encode(userdata)
|
||||||
|
if meta:
|
||||||
|
body["server"]["metadata"] = meta
|
||||||
|
if reservation_id:
|
||||||
|
body["server"][
|
||||||
|
"os-multiple-create:reservation_id"] = reservation_id
|
||||||
|
if key_name:
|
||||||
|
body["server"]["key_name"] = key_name
|
||||||
|
if scheduler_hints:
|
||||||
|
body["server"][
|
||||||
|
"os-scheduler-hints:scheduler_hints"] = scheduler_hints
|
||||||
|
if config_drive:
|
||||||
|
body["server"]["os-config-drive:config_drive"] = config_drive
|
||||||
|
if admin_pass:
|
||||||
|
body["server"]["admin_password"] = admin_pass
|
||||||
|
if not min_count:
|
||||||
|
min_count = 1
|
||||||
|
if not max_count:
|
||||||
|
max_count = min_count
|
||||||
|
body["server"]["os-multiple-create:min_count"] = min_count
|
||||||
|
body["server"]["os-multiple-create:max_count"] = max_count
|
||||||
|
|
||||||
|
if security_groups:
|
||||||
|
body["server"]["os-security-groups:security_groups"] = \
|
||||||
|
[{'name': sg} for sg in security_groups]
|
||||||
|
|
||||||
|
if availability_zone:
|
||||||
|
body["server"][
|
||||||
|
"os-availability-zone:availability_zone"] = availability_zone
|
||||||
|
|
||||||
|
# Block device mappings are passed as a list of dictionaries
|
||||||
|
if block_device_mapping:
|
||||||
|
bdm_param = 'os-block-device-mapping:block_device_mapping'
|
||||||
|
body['server'][bdm_param] = \
|
||||||
|
self._parse_block_device_mapping(block_device_mapping)
|
||||||
|
elif block_device_mapping_v2:
|
||||||
|
# Append the image to the list only if we have new style BDMs
|
||||||
|
if image:
|
||||||
|
bdm_dict = {'uuid': image.id, 'source_type': 'image',
|
||||||
|
'destination_type': 'local', 'boot_index': 0,
|
||||||
|
'delete_on_termination': True}
|
||||||
|
block_device_mapping_v2.insert(0, bdm_dict)
|
||||||
|
|
||||||
|
body['server'][bdm_param] = block_device_mapping_v2
|
||||||
|
|
||||||
|
if nics is not None:
|
||||||
|
# NOTE(tr3buchet): nics can be an empty list
|
||||||
|
all_net_data = []
|
||||||
|
for nic_info in nics:
|
||||||
|
net_data = {}
|
||||||
|
# if value is empty string, do not send value in body
|
||||||
|
if nic_info.get('net-id'):
|
||||||
|
net_data['uuid'] = nic_info['net-id']
|
||||||
|
if nic_info.get('v4-fixed-ip'):
|
||||||
|
net_data['fixed_ip'] = nic_info['v4-fixed-ip']
|
||||||
|
if nic_info.get('port-id'):
|
||||||
|
net_data['port'] = nic_info['port-id']
|
||||||
|
all_net_data.append(net_data)
|
||||||
|
body['server']['networks'] = all_net_data
|
||||||
|
|
||||||
|
return self._create(resource_url, body, response_key,
|
||||||
|
return_raw=return_raw, **kwargs)
|
||||||
|
|
||||||
def get(self, server):
|
def get(self, server):
|
||||||
"""
|
"""
|
||||||
Get a server.
|
Get a server.
|
||||||
@ -623,13 +741,10 @@ class ServerManager(base.BootingManagerWithFind):
|
|||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
if block_device_mapping:
|
if block_device_mapping:
|
||||||
resource_url = "/os-volumes_boot"
|
|
||||||
boot_kwargs['block_device_mapping'] = block_device_mapping
|
boot_kwargs['block_device_mapping'] = block_device_mapping
|
||||||
elif block_device_mapping_v2:
|
elif block_device_mapping_v2:
|
||||||
resource_url = "/os-volumes_boot"
|
|
||||||
boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2
|
boot_kwargs['block_device_mapping_v2'] = block_device_mapping_v2
|
||||||
else:
|
resource_url = "/servers"
|
||||||
resource_url = "/servers"
|
|
||||||
if nics:
|
if nics:
|
||||||
boot_kwargs['nics'] = nics
|
boot_kwargs['nics'] = nics
|
||||||
|
|
||||||
|
@ -74,12 +74,12 @@ def _boot(cs, args, reservation_id=None, min_count=None, max_count=None):
|
|||||||
"be 0")
|
"be 0")
|
||||||
|
|
||||||
if args.image:
|
if args.image:
|
||||||
image = _find_image(cs, args.image)
|
image = _find_image(cs.image_cs, args.image)
|
||||||
else:
|
else:
|
||||||
image = None
|
image = None
|
||||||
|
|
||||||
if not image and args.image_with:
|
if not image and args.image_with:
|
||||||
images = _match_image(cs, args.image_with)
|
images = _match_image(cs.image_cs, args.image_with)
|
||||||
if images:
|
if images:
|
||||||
# TODO(harlowja): log a warning that we
|
# TODO(harlowja): log a warning that we
|
||||||
# are selecting the first of many?
|
# are selecting the first of many?
|
||||||
|
Loading…
Reference in New Issue
Block a user