1
"""Provider interface implementation for OpenStack backend
3
Much of the logic is implemented in sibling modules, but the overall model is
6
Still in need of work here:
7
* Implement constraints using the Nova flavors api. This will always mean an
8
api call rather than hard coding values as is done with EC2. Things like
9
memory and cpu count are broadly equivalent, but there's no guarentee what
10
details are exposed and ranking by price will generally not be an option.
15
from twisted.internet import defer
17
from juju import errors
18
from juju.providers.common.base import MachineProviderBase
20
from .client import _OpenStackClient, _NovaClient, _SwiftClient
21
from . import credentials
22
from .files import FileStorage
23
from .launch import NovaLaunchMachine
24
from .machine import (
25
NovaProviderMachine, get_server_status, machine_from_instance
27
from .ports import NovaPortManager
30
log = logging.getLogger("juju.openstack")
33
class MachineProvider(MachineProviderBase):
34
"""MachineProvider for use in an OpenStack environment"""
36
Credentials = credentials.OpenStackCredentials
38
def __init__(self, environment_name, config):
39
super(MachineProvider, self).__init__(environment_name, config)
40
self.credentials = self.Credentials.from_environment(config)
41
client = _OpenStackClient(self.credentials)
42
self.nova = _NovaClient(client)
43
self.swift = _SwiftClient(client)
44
self.port_manager = NovaPortManager(self.nova, environment_name)
47
def provider_type(self):
50
def get_serialization_data(self):
51
"""Get provider configuration suitable for serialization.
53
Also fills in credential information that may have earlier been
54
extracted from the environment.
56
data = super(MachineProvider, self).get_serialization_data()
57
self.credentials.set_config_defaults(data)
60
def get_file_storage(self):
61
"""Retrieve a Swift-backed :class:`FileStorage`."""
62
return FileStorage(self.swift, self.config["control-bucket"])
64
def start_machine(self, machine_data, master=False):
65
"""Start an OpenStack machine.
67
:param dict machine_data: desired characteristics of the new machine;
68
it must include a "machine-id" key, and may include a "constraints"
69
key to specify the underlying OS and hardware.
71
:param bool master: if True, machine will initialize the juju admin
72
and run a provisioning agent, in addition to running a machine
75
return NovaLaunchMachine.launch(self, machine_data, master)
77
@defer.inlineCallbacks
78
def get_machines(self, instance_ids=()):
79
"""List machines running in the provider.
81
:param list instance_ids: ids of instances you want to get. Leave empty
83
:class:`juju.providers.openstack.machine.NovaProviderMachine` owned
87
:class:`juju.providers.openstack.machine.NovaProviderMachine`
89
:rtype: :class:`twisted.internet.defer.Deferred`
91
:raises: :exc:`juju.errors.MachinesNotFound`
93
if len(instance_ids) == 1:
95
instances = [(yield self.nova.get_server(instance_ids[0]))]
96
except errors.ProviderInteractionError, e:
97
# XXX: Need to wire up treatment of 404s properly in client
98
if True or getattr(e, "kind", None) == "itemNotFound":
99
raise errors.MachinesNotFound(set(instance_ids))
101
instance_ids = frozenset(instance_ids)
103
instances = yield self.nova.list_servers()
105
instance_ids = frozenset(instance_ids)
106
instances = [instance for instance in instances
107
if instance['id'] in instance_ids]
109
# Only want to deal with servers that were created by juju, checking
110
# the name begins with the prefix launch uses is good enough.
111
name_prefix = "juju %s instance " % (self.environment_name,)
113
for instance in instances:
114
if (instance['name'].startswith(name_prefix) and
115
get_server_status(instance) in ("running", "pending")):
116
machines.append(machine_from_instance(instance))
119
# We were asked for a specific list of machines, and if we can't
120
# completely fulfil that request we should blow up.
121
missing = instance_ids.difference(m.instance_id for m in machines)
123
raise errors.MachinesNotFound(missing)
125
defer.returnValue(machines)
127
@defer.inlineCallbacks
128
def _delete_machine(self, machine, full=False):
129
server_id = machine.instance_id
130
server = yield self.nova.get_server(server_id)
131
if not server['name'].startswith(
132
"juju %s instance" % self.environment_name):
133
raise errors.MachinesNotFound(set([machine.instance_id]))
134
yield self.nova.delete_server(server_id)
135
defer.returnValue(machine)
137
def shutdown_machine(self, machine):
138
if not isinstance(machine, NovaProviderMachine):
139
raise errors.ProviderError(
140
"Need a NovaProviderMachine to shutdown not: %r" % (machine,))
141
# EC2 provider re-gets the machine to see if it's still in existance
142
# and can be shutdown, instead just handle an error? 404-ish?
143
return self._delete_machine(machine)
145
@defer.inlineCallbacks
146
def destroy_environment(self):
147
"""Terminate all associated machines and security groups.
149
The super defintion of this method terminates each machine in
150
the environment; this needs to be augmented here by also
151
removing the security group for the environment.
153
:rtype: :class:`twisted.internet.defer.Deferred`
155
machines = yield self.get_machines()
156
deleted_machines = yield defer.gatherResults(
157
[self._delete_machine(m, True) for m in machines])
158
yield self.save_state({})
159
defer.returnValue(deleted_machines)
161
def shutdown_machines(self, machines):
162
"""Terminate machines associated with this provider.
164
:param machines: machines to shut down
165
:type machines: list of
166
:class:`juju.providers.openstack.machine.NovaProviderMachine`
168
:return: list of terminated
169
:class:`juju.providers.openstack.machine.NovaProviderMachine`
171
:rtype: :class:`twisted.internet.defer.Deferred`
173
# XXX: need to actually handle errors as non-terminated machines
174
# and not include them in the resulting list
175
return defer.gatherResults(
176
[self.shutdown_machine(m) for m in machines], consumeErrors=True)
178
def open_port(self, machine, machine_id, port, protocol="tcp"):
179
"""Authorizes `port` using `protocol` on EC2 for `machine`."""
180
return self.port_manager.open_port(machine, machine_id, port, protocol)
182
def close_port(self, machine, machine_id, port, protocol="tcp"):
183
"""Revokes `port` using `protocol` on EC2 for `machine`."""
184
return self.port_manager.close_port(
185
machine, machine_id, port, protocol)
187
def get_opened_ports(self, machine, machine_id):
188
"""Returns a set of open (port, proto) pairs for `machine`."""
189
return self.port_manager.get_opened_ports(machine, machine_id)