summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.openstack.org>2018-09-20 15:30:52 +0000
committerGerrit Code Review <review@openstack.org>2018-09-20 15:30:52 +0000
commita081c594b2532edaf9c4a963cacbb1de3951bd10 (patch)
treea594ded3e653dcfe85cfdb9c05baf3448c4c04e0
parent40dbce7e69dd6893a2c9a8294521f0e4187f5cb5 (diff)
parent29f9f1ffd464c08b53995a3891cdcb5b0867a727 (diff)
Merge "Fleets"
-rw-r--r--iotronic/api/controllers/v1/__init__.py13
-rw-r--r--iotronic/api/controllers/v1/fleet.py347
-rw-r--r--iotronic/api/controllers/v1/utils.py33
-rw-r--r--iotronic/common/exception.py12
-rw-r--r--iotronic/common/policy.py18
-rw-r--r--iotronic/conductor/endpoints.py21
-rw-r--r--iotronic/conductor/rpcapi.py42
-rw-r--r--iotronic/db/api.py51
-rw-r--r--iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py37
-rw-r--r--iotronic/db/sqlalchemy/api.py97
-rw-r--r--iotronic/db/sqlalchemy/models.py16
-rw-r--r--iotronic/objects/__init__.py5
-rw-r--r--iotronic/objects/fleet.py189
13 files changed, 875 insertions, 6 deletions
diff --git a/iotronic/api/controllers/v1/__init__.py b/iotronic/api/controllers/v1/__init__.py
index 573ea25..c911eb3 100644
--- a/iotronic/api/controllers/v1/__init__.py
+++ b/iotronic/api/controllers/v1/__init__.py
@@ -25,6 +25,7 @@ from wsme import types as wtypes
25 25
26from iotronic.api.controllers import base 26from iotronic.api.controllers import base
27from iotronic.api.controllers import link 27from iotronic.api.controllers import link
28from iotronic.api.controllers.v1 import fleet
28from iotronic.api.controllers.v1 import plugin 29from iotronic.api.controllers.v1 import plugin
29from iotronic.api.controllers.v1 import port 30from iotronic.api.controllers.v1 import port
30from iotronic.api.controllers.v1 import service 31from iotronic.api.controllers.v1 import service
@@ -70,6 +71,9 @@ class V1(base.APIBase):
70 ports = [link.Link] 71 ports = [link.Link]
71 """Links to the boards resource""" 72 """Links to the boards resource"""
72 73
74 fleet = [link.Link]
75 """Links to the boards resource"""
76
73 @staticmethod 77 @staticmethod
74 def convert(): 78 def convert():
75 v1 = V1() 79 v1 = V1()
@@ -113,6 +117,14 @@ class V1(base.APIBase):
113 pecan.request.public_url, 'ports', '', 117 pecan.request.public_url, 'ports', '',
114 bookmark=True)] 118 bookmark=True)]
115 119
120 v1.fleets = [link.Link.make_link('self', pecan.request.public_url,
121 'fleets', ''),
122 link.Link.make_link('bookmark',
123 pecan.request.public_url,
124 'fleets', '',
125 bookmark=True)
126 ]
127
116 return v1 128 return v1
117 129
118 130
@@ -123,6 +135,7 @@ class Controller(rest.RestController):
123 plugins = plugin.PluginsController() 135 plugins = plugin.PluginsController()
124 services = service.ServicesController() 136 services = service.ServicesController()
125 ports = port.PortsController() 137 ports = port.PortsController()
138 fleets = fleet.FleetsController()
126 139
127 @expose.expose(V1) 140 @expose.expose(V1)
128 def get(self): 141 def get(self):
diff --git a/iotronic/api/controllers/v1/fleet.py b/iotronic/api/controllers/v1/fleet.py
new file mode 100644
index 0000000..a36b291
--- /dev/null
+++ b/iotronic/api/controllers/v1/fleet.py
@@ -0,0 +1,347 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13
14from iotronic.api.controllers import base
15from iotronic.api.controllers import link
16from iotronic.api.controllers.v1 import collection
17from iotronic.api.controllers.v1 import types
18from iotronic.api.controllers.v1 import utils as api_utils
19from iotronic.api import expose
20from iotronic.common import exception
21from iotronic.common import policy
22from iotronic import objects
23
24import pecan
25from pecan import rest
26import wsme
27from wsme import types as wtypes
28
29_DEFAULT_RETURN_FIELDS = (
30 'name', 'uuid', 'project', 'description', 'extra')
31
32
33class Fleet(base.APIBase):
34 """API representation of a fleet.
35
36 """
37 uuid = types.uuid
38 name = wsme.wsattr(wtypes.text)
39 project = types.uuid
40 description = wsme.wsattr(wtypes.text)
41 extra = types.jsontype
42
43 links = wsme.wsattr([link.Link], readonly=True)
44
45 def __init__(self, **kwargs):
46 self.fields = []
47 fields = list(objects.Fleet.fields)
48 for k in fields:
49 # Skip fields we do not expose.
50 if not hasattr(self, k):
51 continue
52 self.fields.append(k)
53 setattr(self, k, kwargs.get(k, wtypes.Unset))
54
55 @staticmethod
56 def _convert_with_links(fleet, url, fields=None):
57 fleet_uuid = fleet.uuid
58 if fields is not None:
59 fleet.unset_fields_except(fields)
60
61 fleet.links = [link.Link.make_link('self', url, 'fleets',
62 fleet_uuid),
63 link.Link.make_link('bookmark', url, 'fleets',
64 fleet_uuid, bookmark=True)
65 ]
66 return fleet
67
68 @classmethod
69 def convert_with_links(cls, rpc_fleet, fields=None):
70 fleet = Fleet(**rpc_fleet.as_dict())
71
72 if fields is not None:
73 api_utils.check_for_invalid_fields(fields, fleet.as_dict())
74
75 return cls._convert_with_links(fleet, pecan.request.public_url,
76 fields=fields)
77
78
79class FleetCollection(collection.Collection):
80 """API representation of a collection of fleets."""
81
82 fleets = [Fleet]
83 """A list containing fleets objects"""
84
85 def __init__(self, **kwargs):
86 self._type = 'fleets'
87
88 @staticmethod
89 def convert_with_links(fleets, limit, url=None, fields=None, **kwargs):
90 collection = FleetCollection()
91 collection.fleets = [Fleet.convert_with_links(n, fields=fields)
92 for n in fleets]
93 collection.next = collection.get_next(limit, url=url, **kwargs)
94 return collection
95
96
97class PublicFleetsController(rest.RestController):
98 """REST controller for Public Fleets."""
99
100 invalid_sort_key_list = ['extra']
101
102 def _get_fleets_collection(self, marker, limit,
103 sort_key, sort_dir,
104 fields=None):
105
106 limit = api_utils.validate_limit(limit)
107 sort_dir = api_utils.validate_sort_dir(sort_dir)
108
109 marker_obj = None
110 if marker:
111 marker_obj = objects.Fleet.get_by_uuid(pecan.request.context,
112 marker)
113
114 if sort_key in self.invalid_sort_key_list:
115 raise exception.InvalidParameterValue(
116 ("The sort_key value %(key)s is an invalid field for "
117 "sorting") % {'key': sort_key})
118
119 filters = {}
120 filters['public'] = True
121
122 fleets = objects.Fleet.list(pecan.request.context, limit,
123 marker_obj,
124 sort_key=sort_key, sort_dir=sort_dir,
125 filters=filters)
126
127 parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
128
129 return FleetCollection.convert_with_links(fleets, limit,
130 fields=fields,
131 **parameters)
132
133 @expose.expose(FleetCollection, types.uuid, int, wtypes.text,
134 wtypes.text, types.listtype, types.boolean, types.boolean)
135 def get_all(self, marker=None,
136 limit=None, sort_key='id', sort_dir='asc',
137 fields=None):
138 """Retrieve a list of fleets.
139
140 :param marker: pagination marker for large data sets.
141 :param limit: maximum number of resources to return in a single result.
142 This value cannot be larger than the value of max_limit
143 in the [api] section of the ironic configuration, or only
144 max_limit resources will be returned.
145 :param sort_key: column to sort results by. Default: id.
146 :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
147 :param fields: Optional, a list with a specified set of fields
148 of the resource to be returned.
149 """
150 cdict = pecan.request.context.to_policy_values()
151 policy.authorize('iot:fleet:get', cdict, cdict)
152
153 if fields is None:
154 fields = _DEFAULT_RETURN_FIELDS
155 return self._get_fleets_collection(marker,
156 limit, sort_key, sort_dir,
157 fields=fields)
158
159
160class FleetsController(rest.RestController):
161 """REST controller for Fleets."""
162
163 public = PublicFleetsController()
164
165 invalid_sort_key_list = ['extra', ]
166
167 _custom_actions = {
168 'detail': ['GET'],
169 }
170
171 def _get_fleets_collection(self, marker, limit,
172 sort_key, sort_dir,
173 fields=None):
174
175 limit = api_utils.validate_limit(limit)
176 sort_dir = api_utils.validate_sort_dir(sort_dir)
177
178 marker_obj = None
179 if marker:
180 marker_obj = objects.Fleet.get_by_uuid(pecan.request.context,
181 marker)
182
183 if sort_key in self.invalid_sort_key_list:
184 raise exception.InvalidParameterValue(
185 ("The sort_key value %(key)s is an invalid field for "
186 "sorting") % {'key': sort_key})
187
188 filters = {}
189 fleets = objects.Fleet.list(pecan.request.context, limit,
190 marker_obj,
191 sort_key=sort_key, sort_dir=sort_dir,
192 filters=filters)
193
194 parameters = {'sort_key': sort_key, 'sort_dir': sort_dir}
195
196 return FleetCollection.convert_with_links(fleets, limit,
197 fields=fields,
198 **parameters)
199
200 @expose.expose(Fleet, types.uuid_or_name, types.listtype)
201 def get_one(self, fleet_ident, fields=None):
202 """Retrieve information about the given fleet.
203
204 :param fleet_ident: UUID or logical name of a fleet.
205 :param fields: Optional, a list with a specified set of fields
206 of the resource to be returned.
207 """
208
209 rpc_fleet = api_utils.get_rpc_fleet(fleet_ident)
210 cdict = pecan.request.context.to_policy_values()
211 cdict['project'] = rpc_fleet.project
212 policy.authorize('iot:fleet:get_one', cdict, cdict)
213
214 return Fleet.convert_with_links(rpc_fleet, fields=fields)
215
216 @expose.expose(FleetCollection, types.uuid, int, wtypes.text,
217 wtypes.text, types.listtype, types.boolean, types.boolean)
218 def get_all(self, marker=None,
219 limit=None, sort_key='id', sort_dir='asc',
220 fields=None):
221 """Retrieve a list of fleets.
222
223 :param marker: pagination marker for large data sets.
224 :param limit: maximum number of resources to return in a single result.
225 This value cannot be larger than the value of max_limit
226 in the [api] section of the ironic configuration, or only
227 max_limit resources will be returned.
228 :param sort_key: column to sort results by. Default: id.
229 :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
230 :param with_public: Optional boolean to get also public pluings.
231 :param all_fleets: Optional boolean to get all the pluings.
232 Only for the admin
233 :param fields: Optional, a list with a specified set of fields
234 of the resource to be returned.
235 """
236 cdict = pecan.request.context.to_policy_values()
237 policy.authorize('iot:fleet:get', cdict, cdict)
238
239 if fields is None:
240 fields = _DEFAULT_RETURN_FIELDS
241 return self._get_fleets_collection(marker,
242 limit, sort_key, sort_dir,
243 fields=fields)
244
245 @expose.expose(Fleet, body=Fleet, status_code=201)
246 def post(self, Fleet):
247 """Create a new Fleet.
248
249 :param Fleet: a Fleet within the request body.
250 """
251 context = pecan.request.context
252 cdict = context.to_policy_values()
253 policy.authorize('iot:fleet:create', cdict, cdict)
254
255 if not Fleet.name:
256 raise exception.MissingParameterValue(
257 ("Name is not specified."))
258
259 if Fleet.name:
260 if not api_utils.is_valid_name(Fleet.name):
261 msg = ("Cannot create fleet with invalid name %(name)s")
262 raise wsme.exc.ClientSideError(msg % {'name': Fleet.name},
263 status_code=400)
264
265 new_Fleet = objects.Fleet(pecan.request.context,
266 **Fleet.as_dict())
267
268 new_Fleet.project = cdict['project_id']
269 new_Fleet = pecan.request.rpcapi.create_fleet(
270 pecan.request.context,
271 new_Fleet)
272
273 return Fleet.convert_with_links(new_Fleet)
274
275 @expose.expose(None, types.uuid_or_name, status_code=204)
276 def delete(self, fleet_ident):
277 """Delete a fleet.
278
279 :param fleet_ident: UUID or logical name of a fleet.
280 """
281 context = pecan.request.context
282 cdict = context.to_policy_values()
283 policy.authorize('iot:fleet:delete', cdict, cdict)
284
285 rpc_fleet = api_utils.get_rpc_fleet(fleet_ident)
286 pecan.request.rpcapi.destroy_fleet(pecan.request.context,
287 rpc_fleet.uuid)
288
289 @expose.expose(Fleet, types.uuid_or_name, body=Fleet, status_code=200)
290 def patch(self, fleet_ident, val_Fleet):
291 """Update a fleet.
292
293 :param fleet_ident: UUID or logical name of a fleet.
294 :param Fleet: values to be changed
295 :return updated_fleet: updated_fleet
296 """
297
298 rpc_fleet = api_utils.get_rpc_fleet(fleet_ident)
299 cdict = pecan.request.context.to_policy_values()
300 cdict['project'] = rpc_fleet.project
301 policy.authorize('iot:fleet:update', cdict, cdict)
302
303 val_Fleet = val_Fleet.as_dict()
304 for key in val_Fleet:
305 try:
306 rpc_fleet[key] = val_Fleet[key]
307 except Exception:
308 pass
309
310 updated_fleet = pecan.request.rpcapi.update_fleet(
311 pecan.request.context, rpc_fleet)
312 return Fleet.convert_with_links(updated_fleet)
313
314 @expose.expose(FleetCollection, types.uuid, int, wtypes.text,
315 wtypes.text, types.listtype, types.boolean, types.boolean)
316 def detail(self, marker=None,
317 limit=None, sort_key='id', sort_dir='asc',
318 fields=None, with_public=False, all_fleets=False):
319 """Retrieve a list of fleets.
320
321 :param marker: pagination marker for large data sets.
322 :param limit: maximum number of resources to return in a single result.
323 This value cannot be larger than the value of max_limit
324 in the [api] section of the ironic configuration, or only
325 max_limit resources will be returned.
326 :param sort_key: column to sort results by. Default: id.
327 :param sort_dir: direction to sort. "asc" or "desc". Default: asc.
328 :param with_public: Optional boolean to get also public fleet.
329 :param all_fleets: Optional boolean to get all the fleets.
330 Only for the admin
331 :param fields: Optional, a list with a specified set of fields
332 of the resource to be returned.
333 """
334
335 cdict = pecan.request.context.to_policy_values()
336 policy.authorize('iot:fleet:get', cdict, cdict)
337
338 # /detail should only work against collections
339 parent = pecan.request.path.split('/')[:-1][-1]
340 if parent != "fleets":
341 raise exception.HTTPNotFound()
342
343 return self._get_fleets_collection(marker,
344 limit, sort_key, sort_dir,
345 with_public=with_public,
346 all_fleets=all_fleets,
347 fields=fields)
diff --git a/iotronic/api/controllers/v1/utils.py b/iotronic/api/controllers/v1/utils.py
index d330fd6..ecd75a6 100644
--- a/iotronic/api/controllers/v1/utils.py
+++ b/iotronic/api/controllers/v1/utils.py
@@ -156,13 +156,13 @@ def get_rpc_port(port_ident):
156 :raises: InvalidUuidOrName if the name or uuid provided is not valid. 156 :raises: InvalidUuidOrName if the name or uuid provided is not valid.
157 :raises: portNotFound if the port is not found. 157 :raises: portNotFound if the port is not found.
158 """ 158 """
159# Check to see if the port_ident is a valid UUID. If it is, treat it 159 # Check to see if the port_ident is a valid UUID. If it is, treat it
160# as a UUID. 160 # as a UUID.
161 if uuidutils.is_uuid_like(port_ident): 161 if uuidutils.is_uuid_like(port_ident):
162 return objects.Port.get_by_uuid(pecan.request.context, 162 return objects.Port.get_by_uuid(pecan.request.context,
163 port_ident) 163 port_ident)
164 164
165# We can refer to ports by their name, if the client supports it 165 # We can refer to ports by their name, if the client supports it
166 else: 166 else:
167 return objects.Port.get_by_name(pecan.request.context, 167 return objects.Port.get_by_name(pecan.request.context,
168 port_ident) 168 port_ident)
@@ -172,6 +172,33 @@ def get_rpc_port(port_ident):
172 raise exception.PortNottFound(uuid=port_ident) 172 raise exception.PortNottFound(uuid=port_ident)
173 173
174 174
175def get_rpc_fleet(fleet_ident):
176 """Get the RPC fleet from the fleet uuid or logical name.
177
178 :param fleet_ident: the UUID or logical name of a fleet.
179
180 :returns: The RPC Fleet.
181 :raises: InvalidUuidOrName if the name or uuid provided is not valid.
182 :raises: FleetNotFound if the fleet is not found.
183 """
184 # Check to see if the fleet_ident is a valid UUID. If it is, treat it
185 # as a UUID.
186 if uuidutils.is_uuid_like(fleet_ident):
187 return objects.Fleet.get_by_uuid(pecan.request.context,
188 fleet_ident)
189
190 # We can refer to fleets by their name, if the client supports it
191 # if allow_fleet_logical_names():
192 # if utils.is_hostname_safe(fleet_ident):
193 else:
194 return objects.Fleet.get_by_name(pecan.request.context,
195 fleet_ident)
196
197 raise exception.InvalidUuidOrName(name=fleet_ident)
198
199 raise exception.FleetNotFound(fleet=fleet_ident)
200
201
175def is_valid_board_name(name): 202def is_valid_board_name(name):
176 """Determine if the provided name is a valid board name. 203 """Determine if the provided name is a valid board name.
177 204
diff --git a/iotronic/common/exception.py b/iotronic/common/exception.py
index 361fd67..95dd78e 100644
--- a/iotronic/common/exception.py
+++ b/iotronic/common/exception.py
@@ -629,3 +629,15 @@ class NetworkError(IotronicException):
629 629
630class DatabaseVersionTooOld(IotronicException): 630class DatabaseVersionTooOld(IotronicException):
631 _msg_fmt = _("Database version is too old") 631 _msg_fmt = _("Database version is too old")
632
633
634class FleetNotFound(NotFound):
635 message = _("Fleet %(Fleet)s could not be found.")
636
637
638class FleetAlreadyExists(Conflict):
639 message = _("A Fleet with UUID %(uuid)s already exists.")
640
641
642class FleetAlreadyExposed(Conflict):
643 message = _("A Fleet with UUID %(uuid)s already exposed.")
diff --git a/iotronic/common/policy.py b/iotronic/common/policy.py
index ae17eae..122e30d 100644
--- a/iotronic/common/policy.py
+++ b/iotronic/common/policy.py
@@ -104,7 +104,6 @@ plugin_policies = [
104 104
105] 105]
106 106
107
108injection_plugin_policies = [ 107injection_plugin_policies = [
109 policy.RuleDefault('iot:plugin_on_board:get', 108 policy.RuleDefault('iot:plugin_on_board:get',
110 'rule:admin_or_owner', 109 'rule:admin_or_owner',
@@ -166,6 +165,22 @@ exposed_service_policies = [
166 165
167] 166]
168 167
168fleet_policies = [
169 policy.RuleDefault('iot:fleet:get',
170 'rule:is_admin or rule:is_iot_member',
171 description='Retrieve Fleet records'),
172 policy.RuleDefault('iot:fleet:create',
173 'rule:is_iot_member',
174 description='Create Fleet records'),
175 policy.RuleDefault('iot:fleet:get_one', 'rule:admin_or_owner',
176 description='Retrieve a Fleet record'),
177 policy.RuleDefault('iot:fleet:delete', 'rule:admin_or_owner',
178 description='Delete Fleet records'),
179 policy.RuleDefault('iot:fleet:update', 'rule:admin_or_owner',
180 description='Update Fleet records'),
181
182]
183
169 184
170def list_policies(): 185def list_policies():
171 policies = (default_policies 186 policies = (default_policies
@@ -175,6 +190,7 @@ def list_policies():
175 + service_policies 190 + service_policies
176 + exposed_service_policies 191 + exposed_service_policies
177 + port_on_board_policies 192 + port_on_board_policies
193 + fleet_policies
178 ) 194 )
179 return policies 195 return policies
180 196
diff --git a/iotronic/conductor/endpoints.py b/iotronic/conductor/endpoints.py
index fa2a865..302f0ad 100644
--- a/iotronic/conductor/endpoints.py
+++ b/iotronic/conductor/endpoints.py
@@ -26,7 +26,6 @@ from oslo_log import log as logging
26import oslo_messaging 26import oslo_messaging
27import random 27import random
28 28
29
30LOG = logging.getLogger(__name__) 29LOG = logging.getLogger(__name__)
31 30
32serializer = objects_base.IotronicObjectSerializer() 31serializer = objects_base.IotronicObjectSerializer()
@@ -489,3 +488,23 @@ class ConductorEndpoint(object):
489 488
490 except Exception as e: 489 except Exception as e:
491 LOG.error(str(e)) 490 LOG.error(str(e))
491
492 def create_fleet(self, ctx, fleet_obj):
493 new_fleet = serializer.deserialize_entity(ctx, fleet_obj)
494 LOG.debug('Creating fleet %s',
495 new_fleet.name)
496 new_fleet.create()
497 return serializer.serialize_entity(ctx, new_fleet)
498
499 def destroy_fleet(self, ctx, fleet_id):
500 LOG.info('Destroying fleet with id %s',
501 fleet_id)
502 fleet = objects.Fleet.get_by_uuid(ctx, fleet_id)
503 fleet.destroy()
504 return
505
506 def update_fleet(self, ctx, fleet_obj):
507 fleet = serializer.deserialize_entity(ctx, fleet_obj)
508 LOG.debug('Updating fleet %s', fleet.name)
509 fleet.save()
510 return serializer.serialize_entity(ctx, fleet)
diff --git a/iotronic/conductor/rpcapi.py b/iotronic/conductor/rpcapi.py
index 7939038..c8f75b9 100644
--- a/iotronic/conductor/rpcapi.py
+++ b/iotronic/conductor/rpcapi.py
@@ -309,3 +309,45 @@ class ConductorAPI(object):
309 return cctxt.call(context, 'remove_VIF_from_board', 309 return cctxt.call(context, 'remove_VIF_from_board',
310 board_uuid=board_uuid, 310 board_uuid=board_uuid,
311 port_uuid=port_uuid) 311 port_uuid=port_uuid)
312
313 def create_fleet(self, context, fleet_obj, topic=None):
314 """Add a fleet on the cloud
315
316 :param context: request context.
317 :param fleet_obj: a changed (but not saved) fleet object.
318 :param topic: RPC topic. Defaults to self.topic.
319 :returns: created fleet object
320
321 """
322 cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
323 return cctxt.call(context, 'create_fleet',
324 fleet_obj=fleet_obj)
325
326 def destroy_fleet(self, context, fleet_id, topic=None):
327 """Delete a fleet.
328
329 :param context: request context.
330 :param fleet_id: fleet id or uuid.
331 :raises: FleetLocked if fleet is locked by another conductor.
332 :raises: FleetAssociated if the fleet contains an instance
333 associated with it.
334 :raises: InvalidState if the fleet is in the wrong provision
335 state to perform deletion.
336 """
337 cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
338 return cctxt.call(context, 'destroy_fleet', fleet_id=fleet_id)
339
340 def update_fleet(self, context, fleet_obj, topic=None):
341 """Synchronously, have a conductor update the fleet's information.
342
343 Update the fleet's information in the database and
344 return a fleet object.
345
346 :param context: request context.
347 :param fleet_obj: a changed (but not saved) fleet object.
348 :param topic: RPC topic. Defaults to self.topic.
349 :returns: updated fleet object, including all fields.
350
351 """
352 cctxt = self.client.prepare(topic=topic or self.topic, version='1.0')
353 return cctxt.call(context, 'update_fleet', fleet_obj=fleet_obj)
diff --git a/iotronic/db/api.py b/iotronic/db/api.py
index 7c5770e..dc6ec74 100644
--- a/iotronic/db/api.py
+++ b/iotronic/db/api.py
@@ -575,3 +575,54 @@ class Connection(object):
575 575
576 :param port_uuid: The uuid of a port. 576 :param port_uuid: The uuid of a port.
577 """ 577 """
578
579 @abc.abstractmethod
580 def get_fleet_by_id(self, fleet_id):
581 """Return a fleet.
582
583 :param fleet_id: The id of a fleet.
584 :returns: A fleet.
585 """
586
587 @abc.abstractmethod
588 def get_fleet_by_uuid(self, fleet_uuid):
589 """Return a fleet.
590
591 :param fleet_uuid: The uuid of a fleet.
592 :returns: A fleet.
593 """
594
595 @abc.abstractmethod
596 def get_fleet_by_name(self, fleet_name):
597 """Return a fleet.
598
599 :param fleet_name: The logical name of a fleet.
600 :returns: A fleet.
601 """
602
603 @abc.abstractmethod
604 def create_fleet(self, values):
605 """Create a new fleet.
606
607 :param values: A dict containing several items used to identify
608 and track the fleet
609 :returns: A fleet.
610 """
611
612 @abc.abstractmethod
613 def destroy_fleet(self, fleet_id):
614 """Destroy a fleet and all associated interfaces.
615
616 :param fleet_id: The id or uuid of a fleet.
617 """
618
619 @abc.abstractmethod
620 def update_fleet(self, fleet_id, values):
621 """Update properties of a fleet.
622
623 :param fleet_id: The id or uuid of a fleet.
624 :param values: Dict of values to update.
625 :returns: A fleet.
626 :raises: FleetAssociated
627 :raises: FleetNotFound
628 """
diff --git a/iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py b/iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py
new file mode 100644
index 0000000..e56c734
--- /dev/null
+++ b/iotronic/db/sqlalchemy/alembic/versions/b578199e4e64_add_fleets.py
@@ -0,0 +1,37 @@
1# Licensed under the Apache License, Version 2.0 (the "License"); you may
2# not use this file except in compliance with the License. You may obtain
3# a copy of the License at
4#
5# http://www.apache.org/licenses/LICENSE-2.0
6#
7# Unless required by applicable law or agreed to in writing, software
8# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
9# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
10# License for the specific language governing permissions and limitations
11# under the License.
12
13# revision identifiers, used by Alembic.
14revision = 'b578199e4e64'
15down_revision = 'df35e9cbeaff'
16
17from alembic import op
18import iotronic.db.sqlalchemy.models
19import sqlalchemy as sa
20
21
22def upgrade():
23 op.create_table('fleets',
24 sa.Column('created_at', sa.DateTime(), nullable=True),
25 sa.Column('updated_at', sa.DateTime(), nullable=True),
26 sa.Column('id', sa.Integer(), nullable=False),
27 sa.Column('uuid', sa.String(length=36), nullable=True),
28 sa.Column('name', sa.String(length=36), nullable=True),
29 sa.Column('project', sa.String(length=36), nullable=True),
30 sa.Column('description', sa.String(length=300),
31 nullable=True),
32 sa.Column('extra',
33 iotronic.db.sqlalchemy.models.JSONEncodedDict(),
34 nullable=True),
35 sa.PrimaryKeyConstraint('id'),
36 sa.UniqueConstraint('uuid', name='uniq_fleets0uuid')
37 )
diff --git a/iotronic/db/sqlalchemy/api.py b/iotronic/db/sqlalchemy/api.py
index 4055385..a9a96ca 100644
--- a/iotronic/db/sqlalchemy/api.py
+++ b/iotronic/db/sqlalchemy/api.py
@@ -161,6 +161,14 @@ class Connection(api.Connection):
161 query = query.filter(models.Plugin.owner == filters['owner']) 161 query = query.filter(models.Plugin.owner == filters['owner'])
162 return query 162 return query
163 163
164 def _add_fleets_filters(self, query, filters):
165 if filters is None:
166 filters = []
167
168 if 'project' in filters:
169 query = query.filter(models.Fleet.project == filters['project'])
170 return query
171
164 def _add_wampagents_filters(self, query, filters): 172 def _add_wampagents_filters(self, query, filters):
165 if filters is None: 173 if filters is None:
166 filters = [] 174 filters = []
@@ -927,3 +935,92 @@ class Connection(api.Connection):
927 count = query.delete() 935 count = query.delete()
928 if count == 0: 936 if count == 0:
929 raise exception.PortNotFound(uuid=uuid) 937 raise exception.PortNotFound(uuid=uuid)
938
939# FLEET api
940
941 def get_fleet_by_id(self, fleet_id):
942 query = model_query(models.Fleet).filter_by(id=fleet_id)
943 try:
944 return query.one()
945 except NoResultFound:
946 raise exception.FleetNotFound(fleet=fleet_id)
947
948 def get_fleet_by_uuid(self, fleet_uuid):
949 query = model_query(models.Fleet).filter_by(uuid=fleet_uuid)
950 try:
951 return query.one()
952 except NoResultFound:
953 raise exception.FleetNotFound(fleet=fleet_uuid)
954
955 def get_fleet_by_name(self, fleet_name):
956 query = model_query(models.Fleet).filter_by(name=fleet_name)
957 try:
958 return query.one()
959 except NoResultFound:
960 raise exception.FleetNotFound(fleet=fleet_name)
961
962 def destroy_fleet(self, fleet_id):
963
964 session = get_session()
965 with session.begin():
966 query = model_query(models.Fleet, session=session)
967 query = add_identity_filter(query, fleet_id)
968 try:
969 fleet_ref = query.one()
970 except NoResultFound:
971 raise exception.FleetNotFound(fleet=fleet_id)
972
973 # Get fleet ID, if an UUID was supplied. The ID is
974 # required for deleting all ports, attached to the fleet.
975 if uuidutils.is_uuid_like(fleet_id):
976 fleet_id = fleet_ref['id']
977
978 query.delete()
979
980 def update_fleet(self, fleet_id, values):
981 # NOTE(dtantsur): this can lead to very strange errors
982 if 'uuid' in values:
983 msg = _("Cannot overwrite UUID for an existing Fleet.")
984 raise exception.InvalidParameterValue(err=msg)
985
986 try:
987 return self._do_update_fleet(fleet_id, values)
988 except db_exc.DBDuplicateEntry as e:
989 if 'name' in e.columns:
990 raise exception.DuplicateName(name=values['name'])
991 elif 'uuid' in e.columns:
992 raise exception.FleetAlreadyExists(uuid=values['uuid'])
993 else:
994 raise e
995
996 def create_fleet(self, values):
997 # ensure defaults are present for new fleets
998 if 'uuid' not in values:
999 values['uuid'] = uuidutils.generate_uuid()
1000 fleet = models.Fleet()
1001 fleet.update(values)
1002 try:
1003 fleet.save()
1004 except db_exc.DBDuplicateEntry:
1005 raise exception.FleetAlreadyExists(uuid=values['uuid'])
1006 return fleet
1007
1008 def get_fleet_list(self, filters=None, limit=None, marker=None,
1009 sort_key=None, sort_dir=None):
1010 query = model_query(models.Fleet)
1011 query = self._add_fleets_filters(query, filters)
1012 return _paginate_query(models.Fleet, limit, marker,
1013 sort_key, sort_dir, query)
1014
1015 def _do_update_fleet(self, fleet_id, values):
1016 session = get_session()
1017 with session.begin():
1018 query = model_query(models.Fleet, session=session)
1019 query = add_identity_filter(query, fleet_id)
1020 try:
1021 ref = query.with_lockmode('update').one()
1022 except NoResultFound:
1023 raise exception.FleetNotFound(fleet=fleet_id)
1024
1025 ref.update(values)
1026 return ref
diff --git a/iotronic/db/sqlalchemy/models.py b/iotronic/db/sqlalchemy/models.py
index 25555ca..351d411 100644
--- a/iotronic/db/sqlalchemy/models.py
+++ b/iotronic/db/sqlalchemy/models.py
@@ -264,4 +264,20 @@ class Port(Base):
264 ip = Column(String(36)) 264 ip = Column(String(36))
265 # status = Column(String(36)) 265 # status = Column(String(36))
266 network = Column(String(36)) 266 network = Column(String(36))
267
268
267# security_groups = Column(String(40)) 269# security_groups = Column(String(40))
270
271class Fleet(Base):
272 """Represents a fleet."""
273
274 __tablename__ = 'fleets'
275 __table_args__ = (
276 schema.UniqueConstraint('uuid', name='uniq_fleets0uuid'),
277 table_args())
278 id = Column(Integer, primary_key=True)
279 uuid = Column(String(36))
280 name = Column(String(36))
281 project = Column(String(36))
282 description = Column(String(300))
283 extra = Column(JSONEncodedDict)
diff --git a/iotronic/objects/__init__.py b/iotronic/objects/__init__.py
index d0c03e1..499a4f6 100644
--- a/iotronic/objects/__init__.py
+++ b/iotronic/objects/__init__.py
@@ -15,6 +15,7 @@
15from iotronic.objects import board 15from iotronic.objects import board
16from iotronic.objects import conductor 16from iotronic.objects import conductor
17from iotronic.objects import exposedservice 17from iotronic.objects import exposedservice
18from iotronic.objects import fleet
18from iotronic.objects import injectionplugin 19from iotronic.objects import injectionplugin
19from iotronic.objects import location 20from iotronic.objects import location
20from iotronic.objects import plugin 21from iotronic.objects import plugin
@@ -33,6 +34,7 @@ SessionWP = sessionwp.SessionWP
33WampAgent = wampagent.WampAgent 34WampAgent = wampagent.WampAgent
34Service = service.Service 35Service = service.Service
35Port = port.Port 36Port = port.Port
37Fleet = fleet.Fleet
36 38
37__all__ = ( 39__all__ = (
38 Conductor, 40 Conductor,
@@ -44,5 +46,6 @@ __all__ = (
44 Plugin, 46 Plugin,
45 InjectionPlugin, 47 InjectionPlugin,
46 ExposedService, 48 ExposedService,
47 Port 49 Port,
50 Fleet
48) 51)
diff --git a/iotronic/objects/fleet.py b/iotronic/objects/fleet.py
new file mode 100644
index 0000000..67b2723
--- /dev/null
+++ b/iotronic/objects/fleet.py
@@ -0,0 +1,189 @@
1# coding=utf-8
2#
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
15
16from oslo_utils import strutils
17from oslo_utils import uuidutils
18
19from iotronic.common import exception
20from iotronic.db import api as db_api
21from iotronic.objects import base
22from iotronic.objects import utils as obj_utils
23
24
25class Fleet(base.IotronicObject):
26 # Version 1.0: Initial version
27 VERSION = '1.0'
28
29 dbapi = db_api.get_instance()
30
31 fields = {
32 'id': int,
33 'uuid': obj_utils.str_or_none,
34 'name': obj_utils.str_or_none,
35 'project': obj_utils.str_or_none,
36 'description': obj_utils.str_or_none,
37 'extra': obj_utils.dict_or_none,
38 }
39
40 @staticmethod
41 def _from_db_object(fleet, db_fleet):
42 """Converts a database entity to a formal object."""
43 for field in fleet.fields:
44 fleet[field] = db_fleet[field]
45 fleet.obj_reset_changes()
46 return fleet
47
48 @base.remotable_classmethod
49 def get(cls, context, fleet_id):
50 """Find a fleet based on its id or uuid and return a Board object.
51
52 :param fleet_id: the id *or* uuid of a fleet.
53 :returns: a :class:`Board` object.
54 """
55 if strutils.is_int_like(fleet_id):
56 return cls.get_by_id(context, fleet_id)
57 elif uuidutils.is_uuid_like(fleet_id):
58 return cls.get_by_uuid(context, fleet_id)
59 else:
60 raise exception.InvalidIdentity(identity=fleet_id)
61
62 @base.remotable_classmethod
63 def get_by_id(cls, context, fleet_id):
64 """Find a fleet based on its integer id and return a Board object.
65
66 :param fleet_id: the id of a fleet.
67 :returns: a :class:`Board` object.
68 """
69 db_fleet = cls.dbapi.get_fleet_by_id(fleet_id)
70 fleet = Fleet._from_db_object(cls(context), db_fleet)
71 return fleet
72
73 @base.remotable_classmethod
74 def get_by_uuid(cls, context, uuid):
75 """Find a fleet based on uuid and return a Board object.
76
77 :param uuid: the uuid of a fleet.
78 :returns: a :class:`Board` object.
79 """
80 db_fleet = cls.dbapi.get_fleet_by_uuid(uuid)
81 fleet = Fleet._from_db_object(cls(context), db_fleet)
82 return fleet
83
84 @base.remotable_classmethod
85 def get_by_name(cls, context, name):
86 """Find a fleet based on name and return a Board object.
87
88 :param name: the logical name of a fleet.
89 :returns: a :class:`Board` object.
90 """
91 db_fleet = cls.dbapi.get_fleet_by_name(name)
92 fleet = Fleet._from_db_object(cls(context), db_fleet)
93 return fleet
94
95 @base.remotable_classmethod
96 def list(cls, context, limit=None, marker=None, sort_key=None,
97 sort_dir=None, filters=None):
98 """Return a list of Fleet objects.
99
100 :param context: Security context.
101 :param limit: maximum number of resources to return in a single result.
102 :param marker: pagination marker for large data sets.
103 :param sort_key: column to sort results by.
104 :param sort_dir: direction to sort. "asc" or "desc".
105 :param filters: Filters to apply.
106 :returns: a list of :class:`Fleet` object.
107
108 """
109 db_fleets = cls.dbapi.get_fleet_list(filters=filters,
110 limit=limit,
111 marker=marker,
112 sort_key=sort_key,
113 sort_dir=sort_dir)
114 return [Fleet._from_db_object(cls(context), obj)
115 for obj in db_fleets]
116
117 @base.remotable
118 def create(self, context=None):
119 """Create a Fleet record in the DB.
120
121 Column-wise updates will be made based on the result of
122 self.what_changed(). If target_power_state is provided,
123 it will be checked against the in-database copy of the
124 fleet before updates are made.
125
126 :param context: Security context. NOTE: This should only
127 be used internally by the indirection_api.
128 Unfortunately, RPC requires context as the first
129 argument, even though we don't use it.
130 A context should be set when instantiating the
131 object, e.g.: Fleet(context)
132
133 """
134
135 values = self.obj_get_changes()
136 db_fleet = self.dbapi.create_fleet(values)
137 self._from_db_object(self, db_fleet)
138
139 @base.remotable
140 def destroy(self, context=None):
141 """Delete the Fleet from the DB.
142
143 :param context: Security context. NOTE: This should only
144 be used internally by the indirection_api.
145 Unfortunately, RPC requires context as the first
146 argument, even though we don't use it.
147 A context should be set when instantiating the
148 object, e.g.: Fleet(context)
149 """
150 self.dbapi.destroy_fleet(self.uuid)
151 self.obj_reset_changes()
152
153 @base.remotable
154 def save(self, context=None):
155 """Save updates to this Fleet.
156
157 Column-wise updates will be made based on the result of
158 self.what_changed(). If target_power_state is provided,
159 it will be checked against the in-database copy of the
160 fleet before updates are made.
161
162 :param context: Security context. NOTE: This should only
163 be used internally by the indirection_api.
164 Unfortunately, RPC requires context as the first
165 argument, even though we don't use it.
166 A context should be set when instantiating the
167 object, e.g.: Fleet(context)
168 """
169 updates = self.obj_get_changes()
170 self.dbapi.update_fleet(self.uuid, updates)
171 self.obj_reset_changes()
172
173 @base.remotable
174 def refresh(self, context=None):
175 """Refresh the object by re-fetching from the DB.
176
177 :param context: Security context. NOTE: This should only
178 be used internally by the indirection_api.
179 Unfortunately, RPC requires context as the first
180 argument, even though we don't use it.
181 A context should be set when instantiating the
182 object, e.g.: Fleet(context)
183 """
184 current = self.__class__.get_by_uuid(self._context, self.uuid)
185 for field in self.fields:
186 if (hasattr(
187 self, base.get_attrname(field))
188 and self[field] != current[field]):
189 self[field] = current[field]