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:
Chris Yeoh 2013-12-24 21:34:22 +10:30
parent eb0b6d167d
commit c8ad315763
8 changed files with 362 additions and 257 deletions

View File

@ -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)

View File

@ -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:

View File

@ -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')

View File

@ -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
# #

View File

@ -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,
} }
}) })

View File

@ -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.

View File

@ -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

View File

@ -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?