Skip to content
This repository has been archived by the owner on Jun 26, 2020. It is now read-only.

Commit

Permalink
Introduces the Pez instance provider
Browse files Browse the repository at this point in the history
This abstracts away how/where akanda.rug.api.nova.boot_instance()
creates a new resource.  It preserves existing behavior of booting
one-off instances for resources in the OnDemandInstanceProvider.

It introduces a PezInstanceProvider which relies on a new service
akanda-pez-service.  This service is responsible for keeping pools
of hot-standby nodes to be ready for use as appliance VMs.  The size
of said pools are configurable and it maintains separate pools for
each enabled driver type.

When configured with the the PezInstanceProvider, the Rug will request
instances via RPC from Pez instead of booting directly from Nova.

To enable, you must run the akanda-pez-service and then set
'instance_provider=pez' in rug.ini.  The default, which preserves existing
behavior, is 'instance_provider=on_demand'

Implements: blueprint nodepool-support

Change-Id: I7680c747d8c351c63d675f5fb377eee5edbf7b31
  • Loading branch information
gandelman-a committed Oct 21, 2015
1 parent 85d283f commit 96c942e
Show file tree
Hide file tree
Showing 14 changed files with 980 additions and 126 deletions.
18 changes: 15 additions & 3 deletions akanda/rug/api/neutron.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ def from_dict(cls, d):
d['status'],
external_port,
internal_ports,
floating_ips=fips
floating_ips=fips,
)

@property
Expand Down Expand Up @@ -258,7 +258,8 @@ class Port(DictModelBase):
'device_owner', 'name')

def __init__(self, id_, device_id='', fixed_ips=None, mac_address='',
network_id='', device_owner='', name=''):
network_id='', device_owner='', name='',
neutron_port_dict=None):
self.id = id_
self.device_id = device_id
self.fixed_ips = fixed_ips or []
Expand All @@ -267,6 +268,13 @@ def __init__(self, id_, device_id='', fixed_ips=None, mac_address='',
self.device_owner = device_owner
self.name = name

# Unlike instance ports, management ports are created at boot and
# could be created on the Pez side. We need to pass that info
# back to Rug via RPC so hang on to the original port data for
# easier serialization, allowing Rug to re-create (via from_dict).
# without another neutron call.
self._neutron_port_dict = neutron_port_dict or {}

def __eq__(self, other):
return type(self) == type(other) and vars(self) == vars(other)

Expand All @@ -287,7 +295,11 @@ def from_dict(cls, d):
mac_address=d['mac_address'],
network_id=d['network_id'],
device_owner=d['device_owner'],
name=d['name'])
name=d['name'],
neutron_port_dict=d)

def to_dict(self):
return self._neutron_port_dict


class FixedIp(DictModelBase):
Expand Down
174 changes: 144 additions & 30 deletions akanda/rug/api/nova.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@
from oslo_config import cfg
from oslo_log import log as logging

from akanda.rug.common.i18n import _LW, _LE, _LI
from akanda.rug.api import keystone
from akanda.rug.common.i18n import _LW
from akanda.rug.api import neutron
from akanda.rug.pez import rpcapi as pez_api

LOG = logging.getLogger(__name__)

Expand All @@ -33,7 +35,10 @@
'ssh_public_key',
help="Path to the SSH public key for the 'akanda' user within "
"appliance instances",
default='/etc/akanda-rug/akanda.pub')
default='/etc/akanda-rug/akanda.pub'),
cfg.StrOpt(
'instance_provider', default='on_demand',
help='Which instance provider to use (on_demand, pez)')
]
cfg.CONF.register_opts(OPTIONS)

Expand Down Expand Up @@ -98,14 +103,77 @@ def from_nova(cls, instance):
)


class Nova(object):
def __init__(self, conf):
self.conf = conf
ks_session = keystone.KeystoneSession()
self.client = client.Client(version='2', session=ks_session.session)
class InstanceProvider(object):
def __init__(self, client):
self.nova_client = client
LOG.info(_LI(
'Initialized %s with novaclient %s'),
self.__class__.__name__, self.nova_client)

def create_instance(self, driver, name, image_uuid, flavor,
make_ports_callback):
"""Create or get an instance
:param router_id: UUID of the resource that the instance will host
:returns: InstanceInfo object with at least id, name and image_uuid
set.
"""


class PezInstanceProvider(InstanceProvider):
def __init__(self, client):
super(PezInstanceProvider, self).__init__(client)
self.rpc_client = pez_api.AkandaPezAPI(rpc_topic='akanda-pez')
LOG.info(_LI(
'Initialized %s with rpc client %s'),
self.__class__.__name__, self.rpc_client)

def create_instance(self, resource_type, name, image_uuid, flavor,
make_ports_callback):
# TODO(adam_g): pez already creates the mgt port on boot and the one
# we create here is wasted. callback needs to be adjusted
mgt_port, instance_ports = make_ports_callback()

mgt_port_dict = {
'id': mgt_port.id,
'network_id': mgt_port.network_id,
}
instance_ports_dicts = [{
'id': p.id, 'network_id': p.network_id,
} for p in instance_ports]

LOG.debug('Requesting new %s instance from Pez.', resource_type)
pez_instance = self.rpc_client.get_instance(
resource_type, name, mgt_port_dict, instance_ports_dicts)
LOG.debug('Got %s instance %s from Pez.',
resource_type, pez_instance['id'])

server = self.nova_client.servers.get(pez_instance['id'])

# deserialize port data
mgt_port = neutron.Port.from_dict(pez_instance['management_port'])
instance_ports = [
neutron.Port.from_dict(p)
for p in pez_instance['instance_ports']]

boot_time = datetime.strptime(
server.created, "%Y-%m-%dT%H:%M:%SZ")
instance_info = InstanceInfo(
instance_id=server.id,
name=server.name,
management_port=mgt_port,
ports=instance_ports,
image_uuid=image_uuid,
status=server.status,
last_boot=boot_time)

return instance_info


def create_instance(self,
name, image_uuid, flavor, make_ports_callback):
class OnDemandInstanceProvider(InstanceProvider):
def create_instance(self, resource_type, name, image_uuid, flavor,
make_ports_callback):
mgt_port, instance_ports = make_ports_callback()

nics = [{'net-id': p.network_id,
Expand All @@ -116,28 +184,15 @@ def create_instance(self,
LOG.debug('creating instance %s with image %s',
name, image_uuid)

server = self.client.servers.create(
server = self.nova_client.servers.create(
name,
image=image_uuid,
flavor=flavor,
nics=nics,
config_drive=True,
userdata=_format_userdata(mgt_port)
userdata=format_userdata(mgt_port)
)

boot_time = datetime.strptime(
server.created, "%Y-%m-%dT%H:%M:%SZ")
instance_info = InstanceInfo(
instance_id=server.id,
name=name,
management_port=mgt_port,
ports=instance_ports,
image_uuid=image_uuid,
status=server.status,
last_boot=boot_time)

assert server

server_status = None
for i in range(1, 10):
try:
Expand All @@ -149,9 +204,66 @@ def create_instance(self,
time.sleep(.5)
assert server_status

boot_time = datetime.strptime(
server.created, "%Y-%m-%dT%H:%M:%SZ")
instance_info = InstanceInfo(
instance_id=server.id,
name=name,
management_port=mgt_port,
ports=instance_ports,
image_uuid=image_uuid,
status=server.status,
last_boot=boot_time)

instance_info.nova_status = server_status
return instance_info

def get_instance_info(self, name):
"""Retrieves an InstanceInfo object for a given instance name
:param name: name of the instance being queried
:returns: an InstanceInfo object representing the resource instance
"""
instance = self.get_instance_for_obj(name)

if instance:
return InstanceInfo(
instance.id,
name,
image_uuid=instance.image['id']
)


INSTANCE_PROVIDERS = {
'on_demand': OnDemandInstanceProvider,
'pez': PezInstanceProvider,
'default': OnDemandInstanceProvider,
}


def get_instance_provider(provider):
try:
return INSTANCE_PROVIDERS[provider]
except KeyError:
default = INSTANCE_PROVIDERS['default']
LOG.error(_LE('Could not find %s instance provider, using default %s'),
provider, default)
return default


class Nova(object):
def __init__(self, conf):
self.conf = conf
ks_session = keystone.KeystoneSession()
self.client = client.Client(
version='2',
session=ks_session.session,
region_name=conf.auth_region)

self.instance_provider = get_instance_provider(
conf.instance_provider)(self.client)

def get_instance_info(self, name):
"""Retrieves an InstanceInfo object for a given instance name
Expand Down Expand Up @@ -198,6 +310,7 @@ def destroy_instance(self, instance_info):
self.client.servers.delete(instance_info.id_)

def boot_instance(self,
resource_type,
prev_instance_info,
name,
image_uuid,
Expand Down Expand Up @@ -225,11 +338,12 @@ def boot_instance(self,
return None

# it is now safe to attempt boot
instance_info = self.create_instance(
name,
image_uuid,
flavor,
make_ports_callback
instance_info = self.instance_provider.create_instance(
resource_type=resource_type,
name=name,
image_uuid=image_uuid,
flavor=flavor,
make_ports_callback=make_ports_callback
)
return instance_info

Expand Down Expand Up @@ -286,7 +400,7 @@ def _ssh_key():
return ''


def _format_userdata(mgt_port):
def format_userdata(mgt_port):
ctxt = {
'ssh_public_key': _ssh_key(),
'mac_address': mgt_port.mac_address,
Expand Down
2 changes: 1 addition & 1 deletion akanda/rug/common/rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ def get_target(topic, fanout=True, exchange=None, version=None, server=None):
server=server)


def get_rpc_client(topic, exchange, version='1.0'):
def get_rpc_client(topic, exchange=None, version='1.0'):
"""Creates an RPC client to be used to request methods be
executed on remote RPC servers
"""
Expand Down
2 changes: 1 addition & 1 deletion akanda/rug/db/sqlalchemy/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
help=_('MySQL engine to use.'))
]

_DEFAULT_SQL_CONNECTION = 'sqlite:///akanda-ruxg.db'
_DEFAULT_SQL_CONNECTION = 'sqlite:///akanda-rug.db'


cfg.CONF.register_opts(sql_opts, 'database')
Expand Down
11 changes: 6 additions & 5 deletions akanda/rug/instance_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,11 +230,12 @@ def boot(self, worker_context):
# try to boot the instance
try:
instance_info = worker_context.nova_client.boot_instance(
self.instance_info,
self.driver.name,
self.driver.image_uuid,
self.driver.flavor,
self.driver.make_ports(worker_context)
resource_type=self.driver.RESOURCE_NAME,
prev_instance_info=self.instance_info,
name=self.driver.name,
image_uuid=self.driver.image_uuid,
flavor=self.driver.flavor,
make_ports_callback=self.driver.make_ports(worker_context)
)
if not instance_info:
self.log.info(_LI('Previous instance is still deleting'))
Expand Down
Empty file added akanda/rug/pez/__init__.py
Empty file.

0 comments on commit 96c942e

Please sign in to comment.