Open Kilda Java Documentation
rest.py
Go to the documentation of this file.
1 # Copyright 2017 Telstra Open Source
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 
16 import collections
17 import functools
18 import json
19 import multiprocessing.pool
20 import uuid
21 from wsgiref.simple_server import WSGIServer, WSGIRequestHandler
22 
23 import bottle
24 
25 from kilda.traffexam import exc
26 from kilda.traffexam import model
27 
28 app = bottle.Bottle(autojson=False)
29 app.install(bottle.JSONPlugin(
30  json_dumps=functools.partial(json.dumps, cls=model.JSONEncoder)))
31 config_key = 'traffexam.{}'.format
32 
33 
34 @app.route('/address', methpd='GET')
36  context = get_context()
37  return address_response(context.service.address.list())
38 
39 
40 @app.route('/address', method='POST')
42  context = get_context()
43 
44  payload = bottle.request.json
45  payload.pop('idnr', None)
46 
47  address = extract_payload_fields(payload, 'address')[0]
48  vlan_tag = payload.pop('vlan', 0)
49  if vlan_tag:
50  try:
51  vlan = context.service.vlan.lookup(vlan_tag)
53  vlan = model.VLAN(vlan_tag)
54  context.service.vlan.create(vlan)
55 
56  iface = vlan.iface
57  else:
58  iface = None
59 
60  try:
61  entity = model.IpAddress(address, iface=iface, **payload)
62  except TypeError as e:
63  return bottle.HTTPError(400, str(e))
64 
65  try:
66  context.service.address.create(entity)
68  return bottle.HTTPError(400, str(e))
69 
70  bottle.response.status = 201
71  return address_response(entity)
72 
73 
74 @app.route('/address/<idnr>', method='GET')
75 def address_read(idnr):
76  context = get_context()
77  idnr = unpack_idnr(idnr)
78  try:
79  address = context.service.address.lookup(idnr)
81  return bottle.HTTPError(404, 'Address does not exist.')
82  return address_response(address)
83 
84 
85 @app.route('/address/<idnr>', method='DELETE')
86 def address_delete(idnr):
87  context = get_context()
88  idnr = unpack_idnr(idnr)
89  try:
90  context.service.address.delete(idnr)
92  return bottle.HTTPError(404, 'Address does not exist.')
93  bottle.response.status = 204
94 
95 
96 def address_response(payload):
97  return format_response(payload, 'address', 'addresses')
98 
99 
100 @app.route('/endpoint', method='GET')
102  context = get_context()
103  return endpoint_response(context.service.endpoint.list())
104 
105 
106 @app.route('/endpoint', method='POST')
108  context = get_context()
109  payload = bottle.request.json
110  payload.pop('idnr', None)
111 
112  kind = extract_payload_fields(payload, 'type')[0]
113  try:
114  klass = model.EndpointKind(kind)
115  klass = model.endpoint_klass_map[klass]
116  except (TypeError, KeyError):
117  return bottle.HTTPError(400, 'Invalid endpoint type {!r}'.format(kind))
118 
119  try:
120  bind_address = payload.pop('bind_address', None)
121  if bind_address:
122  bind_address = unpack_idnr(bind_address)
123  bind_address = context.service.address.lookup(bind_address)
124 
125  if klass is model.ConsumerEndpoint:
126  if bind_address is None:
127  raise bottle.HTTPError(400, "bind_address field is missing")
128  entity = klass(bind_address, **payload)
129  elif klass is model.ProducerEndpoint:
130  address = extract_payload_fields(
131  payload, 'remote_address')[0]
132  address, port = extract_payload_fields(address, 'address', 'port')
133  entity = klass(
134  model.EndpointAddress(address, port),
135  bind_address=bind_address, **payload)
136  else:
137  raise bottle.HTTPError(500, 'Unreachable point have been reached!')
138  except exc.ServiceLookupError as e:
139  return bottle.HTTPError(400, 'Invalid resource reference: {}'.format(e))
140  except TypeError as e:
141  return bottle.HTTPError(400, str(e))
142 
143  context.service.endpoint.create(entity)
144 
145  bottle.response.status = 201
146  return endpoint_response(entity)
147 
148 
149 @app.route('/endpoint/<idnr>', method='GET')
150 def endpoint_read(idnr):
151  context = get_context()
152  idnr = unpack_idnr(idnr)
153  try:
154  entity = context.service.endpoint.lookup(idnr)
155  except exc.ServiceLookupError:
156  return bottle.HTTPError(404, 'Endpoint does not exist.')
157  return endpoint_response(entity)
158 
159 
160 @app.route('/endpoint/<idnr>', method='DELETE')
161 def endpoint_delete(idnr):
162  context = get_context()
163  idnr = unpack_idnr(idnr)
164  try:
165  context.service.endpoint.delete(idnr)
166  except exc.ServiceLookupError:
167  return bottle.HTTPError(404, 'Endpoint does not exist.')
168  bottle.response.status = 204
169 
170 
171 @app.route('/endpoint/<idnr>/report', method='GET')
173  context = get_context()
174  idnr = unpack_idnr(idnr)
175  try:
176  output = context.service.endpoint.get_report(idnr)
177  if output:
178  report, error = output
179  else:
180  report = error = None
181  entity = context.service.endpoint.lookup(idnr)
182  except exc.ServiceLookupError:
183  return bottle.HTTPError(404, 'Endpoint does not exist')
184 
185  return {
186  'report': report,
187  'error': error,
188  'status': entity.proc.returncode}
189 
190 
191 def endpoint_response(payload):
192  return format_response(payload, 'endpoint', 'endpoints')
193 
194 
195 def format_response(payload, single, multiple):
196  key = single
197  if isinstance(payload, collections.Sequence):
198  key = multiple
199  return {key: payload}
200 
201 
202 def init(bind, context):
203  app.config[config_key('context')] = context
204  app.run(server=MTServer, host=bind.address, port=bind.port, thread_count=5)
205 
206 
207 def unpack_idnr(idnr):
208  try:
209  idnr = uuid.UUID(idnr)
210  except ValueError:
211  raise bottle.HTTPError(400, 'Invalid resource id')
212  return idnr
213 
214 
216  return bottle.request.app.config[config_key('context')]
217 
218 
219 def extract_payload_fields(payload, *fields):
220  missing = set()
221  found = []
222 
223  for name in fields:
224  try:
225  found.append(payload.pop(name))
226  except KeyError:
227  missing.add(name)
228 
229  if missing:
230  raise bottle.HTTPError(400, 'Payload is lack of fields: "{}"'.format(
231  '", "'.join(sorted(missing))))
232 
233  return found
234 
235 
236 # All that below is taken from https://github.com/RonRothman/mtwsgi
237 #
238 # Can make "normal" dependency doe to lack of "packaging" stuff in this repo.
239 #
240 # rev: a8f67cfc0d538714a612f78e39c9e1148725ea73
241 # license: The MIT License(MIT)
242 
243 class ThreadPoolWSGIServer(WSGIServer):
244  '''WSGI-compliant HTTP server. Dispatches requests to a pool of threads.'''
245 
246  def __init__(self, thread_count=None, *args, **kwargs):
247  '''If 'thread_count' == None, we'll use multiprocessing.cpu_count() threads.'''
248  WSGIServer.__init__(self, *args, **kwargs)
249  self.thread_count = thread_count
250  self.pool = multiprocessing.pool.ThreadPool(self.thread_count)
251 
252  # Inspired by SocketServer.ThreadingMixIn.
253  def process_request_thread(self, request, client_address):
254  try:
255  self.finish_request(request, client_address)
256  self.shutdown_request(request)
257  except:
258  self.handle_error(request, client_address)
259  self.shutdown_request(request)
260 
261  def process_request(self, request, client_address):
262  self.pool.apply_async(self.process_request_thread,
263  args=(request, client_address))
264 
265 
266 class MTServer(bottle.ServerAdapter):
267  def run(self, handler):
268  thread_count = self.options.pop('thread_count', None)
269  server = make_server(
270  self.host, self.port, handler, thread_count, **self.options)
271  server.serve_forever()
272 
273 
274 def make_server(host, port, app, thread_count=None,
275  handler_class=WSGIRequestHandler):
276  '''Create a new WSGI server listening on `host` and `port` for `app`'''
277  httpd = ThreadPoolWSGIServer(thread_count, (host, port), handler_class)
278  httpd.set_app(app)
279  return httpd
def format_response(payload, single, multiple)
Definition: rest.py:195
def make_server(host, port, app, thread_count=None, handler_class=WSGIRequestHandler)
Definition: rest.py:275
string config_key
Definition: rest.py:31
def address_read(idnr)
Definition: rest.py:75
def init(bind, context)
Definition: rest.py:202
def extract_payload_fields(payload, fields)
Definition: rest.py:219
def address_delete(idnr)
Definition: rest.py:86
def __init__(self, thread_count=None, args, kwargs)
Definition: rest.py:246
def endpoint_delete(idnr)
Definition: rest.py:161
def process_request_thread(self, request, client_address)
Definition: rest.py:253
def unpack_idnr(idnr)
Definition: rest.py:207
def endpoint_do_report(idnr)
Definition: rest.py:172
def address_create()
Definition: rest.py:41
def address_response(payload)
Definition: rest.py:96
def endpoint_read(idnr)
Definition: rest.py:150
def process_request(self, request, client_address)
Definition: rest.py:261
def endpoint_response(payload)
Definition: rest.py:191
def run(self, handler)
Definition: rest.py:267
def endpoint_create()
Definition: rest.py:107
def get_context()
Definition: rest.py:215
def address_list()
Definition: rest.py:35
def endpoint_list()
Definition: rest.py:101