Revision 6d81475c
b/Makefile.am | ||
---|---|---|
11 | 11 |
|
12 | 12 |
ACLOCAL_AMFLAGS = -I autotools |
13 | 13 |
DOCBOOK_WRAPPER = $(top_srcdir)/autotools/docbook-wrapper |
14 |
BUILD_RAPI_RESOURCE_DOC = $(top_srcdir)/doc/build-rapi-resources-doc |
|
15 | 14 |
REPLACE_VARS_SED = autotools/replace_vars.sed |
16 | 15 |
|
17 | 16 |
hypervisordir = $(pkgpythondir)/hypervisor |
... | ... | |
45 | 44 |
CLEANFILES = \ |
46 | 45 |
autotools/replace_vars.sed \ |
47 | 46 |
devel/upload \ |
48 |
doc/rapi-resources.gen \ |
|
49 | 47 |
doc/examples/bash_completion \ |
50 | 48 |
doc/examples/ganeti.initd \ |
51 | 49 |
doc/examples/ganeti.cron \ |
... | ... | |
114 | 112 |
doc/hooks.rst \ |
115 | 113 |
doc/iallocator.rst \ |
116 | 114 |
doc/install.rst \ |
115 |
doc/rapi.rst \ |
|
117 | 116 |
doc/security.rst |
118 | 117 |
|
119 | 118 |
dochtml = $(patsubst %.rst,%.html,$(docrst)) |
... | ... | |
152 | 151 |
devel/upload.in \ |
153 | 152 |
$(docrst) \ |
154 | 153 |
$(docdot) \ |
155 |
doc/build-rapi-resources-doc \ |
|
156 | 154 |
doc/examples/bash_completion.in \ |
157 | 155 |
doc/examples/ganeti.initd.in \ |
158 | 156 |
doc/examples/ganeti.cron.in \ |
... | ... | |
224 | 222 |
|
225 | 223 |
TESTS_ENVIRONMENT = PYTHONPATH=.:$(top_builddir) |
226 | 224 |
|
227 |
RAPI_RESOURCES = $(wildcard lib/rapi/*.py) |
|
228 |
|
|
229 | 225 |
all-local: stamp-directories lib/_autoconf.py devel/upload \ |
230 | 226 |
doc/examples/bash_completion \ |
231 | 227 |
doc/examples/ganeti.initd doc/examples/ganeti.cron |
... | ... | |
248 | 244 |
|
249 | 245 |
doc/design-2.0.html: doc/design-2.0.rst doc/arch-2.0.png |
250 | 246 |
|
251 |
doc/rapi.html: doc/rapi-resources.gen |
|
252 |
|
|
253 |
doc/rapi-resources.gen: $(BUILD_RAPI_RESOURCE_DOC) $(RAPI_RESOURCES) |
|
254 |
PYTHONPATH=.:$(top_builddir) $(BUILD_RAPI_RESOURCE_DOC) > $@ || \ |
|
255 |
rm -f $@ |
|
256 |
|
|
257 | 247 |
man/%.7.in man/%.8.in: man/%.sgml man/footer.sgml $(DOCBOOK_WRAPPER) |
258 | 248 |
@test -n "$(DOCBOOK2MAN)" || { echo 'docbook2html' not found during configure; exit 1; } |
259 | 249 |
TMPDIR=`mktemp -d` && { \ |
... | ... | |
281 | 271 |
|
282 | 272 |
man/footer.sgml $(TESTS): srclinks |
283 | 273 |
|
284 |
$(TESTS) $(BUILD_RAPI_RESOURCE_DOC): ganeti lib/_autoconf.py
|
|
274 |
$(TESTS): ganeti lib/_autoconf.py |
|
285 | 275 |
|
286 | 276 |
lib/_autoconf.py: Makefile stamp-directories |
287 | 277 |
set -e; \ |
/dev/null | ||
---|---|---|
1 |
#!/usr/bin/python |
|
2 |
# |
|
3 |
|
|
4 |
# Copyright (C) 2008 Google Inc. |
|
5 |
# |
|
6 |
# This program is free software; you can redistribute it and/or modify |
|
7 |
# it under the terms of the GNU General Public License as published by |
|
8 |
# the Free Software Foundation; either version 2 of the License, or |
|
9 |
# (at your option) any later version. |
|
10 |
# |
|
11 |
# This program is distributed in the hope that it will be useful, but |
|
12 |
# WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
|
14 |
# General Public License for more details. |
|
15 |
# |
|
16 |
# You should have received a copy of the GNU General Public License |
|
17 |
# along with this program; if not, write to the Free Software |
|
18 |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA |
|
19 |
# 02110-1301, USA. |
|
20 |
|
|
21 |
"""Script to generate documentation for remote API resources. |
|
22 |
|
|
23 |
This is hard-coded to the section numbering we have in the master RST |
|
24 |
document. |
|
25 |
|
|
26 |
""" |
|
27 |
|
|
28 |
import re |
|
29 |
import cgi |
|
30 |
import inspect |
|
31 |
|
|
32 |
from ganeti.rapi import rlib2 |
|
33 |
from ganeti.rapi import connector |
|
34 |
|
|
35 |
|
|
36 |
CHECKED_COMMANDS = ["GET", "POST", "PUT", "DELETE"] |
|
37 |
|
|
38 |
|
|
39 |
def beautify(text): |
|
40 |
"""A couple of small enhancements, epydoc-to-rst. |
|
41 |
|
|
42 |
""" |
|
43 |
pairs = [ |
|
44 |
("@return:", "Returns:"), |
|
45 |
] |
|
46 |
|
|
47 |
for old, new in pairs: |
|
48 |
text = text.replace(old, new) |
|
49 |
|
|
50 |
return text |
|
51 |
|
|
52 |
|
|
53 |
def indent(text): |
|
54 |
"""Returns a text block with all lines indented. |
|
55 |
|
|
56 |
""" |
|
57 |
lines = text.splitlines() |
|
58 |
lines = [" " + l for l in lines] |
|
59 |
return "\n".join(lines) |
|
60 |
|
|
61 |
|
|
62 |
def main(): |
|
63 |
# Get list of all resources |
|
64 |
all = list(connector.CONNECTOR.itervalues()) |
|
65 |
|
|
66 |
# Sort rlib by URI |
|
67 |
all.sort(cmp=lambda a, b: cmp(a.DOC_URI, b.DOC_URI)) |
|
68 |
|
|
69 |
print ".. Automatically generated, do not edit\n" |
|
70 |
|
|
71 |
for cls in all: |
|
72 |
title = cls.DOC_URI |
|
73 |
print "%s\n%s\n" % (title, "+" * len(title)) |
|
74 |
|
|
75 |
# Class docstring |
|
76 |
description = inspect.getdoc(cls) |
|
77 |
if description: |
|
78 |
print "::\n" |
|
79 |
print indent(description.strip()) |
|
80 |
|
|
81 |
supported = [cmd for cmd in CHECKED_COMMANDS if hasattr(cls, cmd)] |
|
82 |
print "It supports the following commands: %s." % (", ".join(supported)) |
|
83 |
|
|
84 |
|
|
85 |
for cmd in CHECKED_COMMANDS: |
|
86 |
if not hasattr(cls, cmd): |
|
87 |
continue |
|
88 |
|
|
89 |
print "%s\n%s\n" % (cmd, "~" * len(cmd)) |
|
90 |
|
|
91 |
# Get docstring |
|
92 |
text = inspect.getdoc(getattr(cls, cmd)) |
|
93 |
if text: |
|
94 |
text = beautify(text) |
|
95 |
print "::\n" |
|
96 |
print indent(text) |
|
97 |
|
|
98 |
|
|
99 |
|
|
100 |
if __name__ == "__main__": |
|
101 |
main() |
b/doc/rapi.rst | ||
---|---|---|
82 | 82 |
Resources |
83 | 83 |
--------- |
84 | 84 |
|
85 |
.. include:: rapi-resources.gen |
|
85 |
/ |
|
86 |
+ |
|
87 |
|
|
88 |
:: |
|
89 |
|
|
90 |
/ resource. |
|
91 |
|
|
92 |
It supports the following commands: GET. |
|
93 |
|
|
94 |
GET |
|
95 |
~~~ |
|
96 |
|
|
97 |
:: |
|
98 |
|
|
99 |
Show the list of mapped resources. |
|
100 |
|
|
101 |
Returns: a dictionary with 'name' and 'uri' keys for each of them. |
|
102 |
|
|
103 |
/2 |
|
104 |
++ |
|
105 |
|
|
106 |
:: |
|
107 |
|
|
108 |
/2 resource, the root of the version 2 API. |
|
109 |
|
|
110 |
It supports the following commands: GET. |
|
111 |
|
|
112 |
GET |
|
113 |
~~~ |
|
114 |
|
|
115 |
:: |
|
116 |
|
|
117 |
Show the list of mapped resources. |
|
118 |
|
|
119 |
Returns: a dictionary with 'name' and 'uri' keys for each of them. |
|
120 |
|
|
121 |
/2/info |
|
122 |
+++++++ |
|
123 |
|
|
124 |
:: |
|
125 |
|
|
126 |
Cluster info. |
|
127 |
|
|
128 |
It supports the following commands: GET. |
|
129 |
|
|
130 |
GET |
|
131 |
~~~ |
|
132 |
|
|
133 |
:: |
|
134 |
|
|
135 |
Returns cluster information. |
|
136 |
|
|
137 |
Example:: |
|
138 |
|
|
139 |
{ |
|
140 |
"config_version": 2000000, |
|
141 |
"name": "cluster", |
|
142 |
"software_version": "2.0.0~beta2", |
|
143 |
"os_api_version": 10, |
|
144 |
"export_version": 0, |
|
145 |
"candidate_pool_size": 10, |
|
146 |
"enabled_hypervisors": [ |
|
147 |
"fake" |
|
148 |
], |
|
149 |
"hvparams": { |
|
150 |
"fake": {} |
|
151 |
}, |
|
152 |
"default_hypervisor": "fake", |
|
153 |
"master": "node1.example.com", |
|
154 |
"architecture": [ |
|
155 |
"64bit", |
|
156 |
"x86_64" |
|
157 |
], |
|
158 |
"protocol_version": 20, |
|
159 |
"beparams": { |
|
160 |
"default": { |
|
161 |
"auto_balance": true, |
|
162 |
"vcpus": 1, |
|
163 |
"memory": 128 |
|
164 |
} |
|
165 |
} |
|
166 |
} |
|
167 |
|
|
168 |
/2/instances |
|
169 |
++++++++++++ |
|
170 |
|
|
171 |
:: |
|
172 |
|
|
173 |
/2/instances resource. |
|
174 |
|
|
175 |
It supports the following commands: GET, POST. |
|
176 |
|
|
177 |
GET |
|
178 |
~~~ |
|
179 |
|
|
180 |
:: |
|
181 |
|
|
182 |
Returns a list of all available instances. |
|
183 |
|
|
184 |
|
|
185 |
Example:: |
|
186 |
|
|
187 |
[ |
|
188 |
{ |
|
189 |
"name": "web.example.com", |
|
190 |
"uri": "\/instances\/web.example.com" |
|
191 |
}, |
|
192 |
{ |
|
193 |
"name": "mail.example.com", |
|
194 |
"uri": "\/instances\/mail.example.com" |
|
195 |
} |
|
196 |
] |
|
197 |
|
|
198 |
If the optional 'bulk' argument is provided and set to 'true' |
|
199 |
value (i.e '?bulk=1'), the output contains detailed |
|
200 |
information about instances as a list. |
|
201 |
|
|
202 |
Example:: |
|
203 |
|
|
204 |
[ |
|
205 |
{ |
|
206 |
"status": "running", |
|
207 |
"disk_usage": 20480, |
|
208 |
"nic.bridges": [ |
|
209 |
"xen-br0" |
|
210 |
], |
|
211 |
"name": "web.example.com", |
|
212 |
"tags": ["tag1", "tag2"], |
|
213 |
"beparams": { |
|
214 |
"vcpus": 2, |
|
215 |
"memory": 512 |
|
216 |
}, |
|
217 |
"disk.sizes": [ |
|
218 |
20480 |
|
219 |
], |
|
220 |
"pnode": "node1.example.com", |
|
221 |
"nic.macs": ["01:23:45:67:89:01"], |
|
222 |
"snodes": ["node2.example.com"], |
|
223 |
"disk_template": "drbd", |
|
224 |
"admin_state": true, |
|
225 |
"os": "debian-etch", |
|
226 |
"oper_state": true |
|
227 |
}, |
|
228 |
... |
|
229 |
] |
|
230 |
|
|
231 |
Returns: a dictionary with 'name' and 'uri' keys for each of them. |
|
232 |
|
|
233 |
POST |
|
234 |
~~~~ |
|
235 |
|
|
236 |
:: |
|
237 |
|
|
238 |
Create an instance. |
|
239 |
|
|
240 |
Returns: a job id |
|
241 |
|
|
242 |
/2/instances/[instance_name] |
|
243 |
++++++++++++++++++++++++++++ |
|
244 |
|
|
245 |
:: |
|
246 |
|
|
247 |
/2/instances/[instance_name] resources. |
|
248 |
|
|
249 |
It supports the following commands: GET, DELETE. |
|
250 |
|
|
251 |
GET |
|
252 |
~~~ |
|
253 |
|
|
254 |
:: |
|
255 |
|
|
256 |
Send information about an instance. |
|
257 |
|
|
258 |
|
|
259 |
|
|
260 |
DELETE |
|
261 |
~~~~~~ |
|
262 |
|
|
263 |
:: |
|
264 |
|
|
265 |
Delete an instance. |
|
266 |
|
|
267 |
|
|
268 |
|
|
269 |
/2/instances/[instance_name]/reboot |
|
270 |
+++++++++++++++++++++++++++++++++++ |
|
271 |
|
|
272 |
:: |
|
273 |
|
|
274 |
/2/instances/[instance_name]/reboot resource. |
|
275 |
|
|
276 |
Implements an instance reboot. |
|
277 |
|
|
278 |
It supports the following commands: POST. |
|
279 |
|
|
280 |
POST |
|
281 |
~~~~ |
|
282 |
|
|
283 |
:: |
|
284 |
|
|
285 |
Reboot an instance. |
|
286 |
|
|
287 |
The URI takes type=[hard|soft|full] and |
|
288 |
ignore_secondaries=[False|True] parameters. |
|
289 |
|
|
290 |
/2/instances/[instance_name]/shutdown |
|
291 |
+++++++++++++++++++++++++++++++++++++ |
|
292 |
|
|
293 |
:: |
|
294 |
|
|
295 |
/2/instances/[instance_name]/shutdown resource. |
|
296 |
|
|
297 |
Implements an instance shutdown. |
|
298 |
|
|
299 |
It supports the following commands: PUT. |
|
300 |
|
|
301 |
PUT |
|
302 |
~~~ |
|
303 |
|
|
304 |
:: |
|
305 |
|
|
306 |
Shutdown an instance. |
|
307 |
|
|
308 |
|
|
309 |
|
|
310 |
/2/instances/[instance_name]/startup |
|
311 |
++++++++++++++++++++++++++++++++++++ |
|
312 |
|
|
313 |
:: |
|
314 |
|
|
315 |
/2/instances/[instance_name]/startup resource. |
|
316 |
|
|
317 |
Implements an instance startup. |
|
318 |
|
|
319 |
It supports the following commands: PUT. |
|
320 |
|
|
321 |
PUT |
|
322 |
~~~ |
|
323 |
|
|
324 |
:: |
|
325 |
|
|
326 |
Startup an instance. |
|
327 |
|
|
328 |
The URI takes force=[False|True] parameter to start the instance |
|
329 |
if even if secondary disks are failing. |
|
330 |
|
|
331 |
/2/instances/[instance_name]/tags |
|
332 |
+++++++++++++++++++++++++++++++++ |
|
333 |
|
|
334 |
:: |
|
335 |
|
|
336 |
/2/instances/[instance_name]/tags resource. |
|
337 |
|
|
338 |
Manages per-instance tags. |
|
339 |
|
|
340 |
It supports the following commands: GET, PUT, DELETE. |
|
341 |
|
|
342 |
GET |
|
343 |
~~~ |
|
344 |
|
|
345 |
:: |
|
346 |
|
|
347 |
Returns a list of tags. |
|
348 |
|
|
349 |
Example: ["tag1", "tag2", "tag3"] |
|
350 |
|
|
351 |
PUT |
|
352 |
~~~ |
|
353 |
|
|
354 |
:: |
|
355 |
|
|
356 |
Add a set of tags. |
|
357 |
|
|
358 |
The request as a list of strings should be PUT to this URI. And |
|
359 |
you'll have back a job id. |
|
360 |
|
|
361 |
DELETE |
|
362 |
~~~~~~ |
|
363 |
|
|
364 |
:: |
|
365 |
|
|
366 |
Delete a tag. |
|
367 |
|
|
368 |
In order to delete a set of tags, the DELETE |
|
369 |
request should be addressed to URI like: |
|
370 |
/tags?tag=[tag]&tag=[tag] |
|
371 |
|
|
372 |
/2/jobs |
|
373 |
+++++++ |
|
374 |
|
|
375 |
:: |
|
376 |
|
|
377 |
/2/jobs resource. |
|
378 |
|
|
379 |
It supports the following commands: GET. |
|
380 |
|
|
381 |
GET |
|
382 |
~~~ |
|
383 |
|
|
384 |
:: |
|
385 |
|
|
386 |
Returns a dictionary of jobs. |
|
387 |
|
|
388 |
Returns: a dictionary with jobs id and uri. |
|
389 |
|
|
390 |
/2/jobs/[job_id] |
|
391 |
++++++++++++++++ |
|
392 |
|
|
393 |
:: |
|
394 |
|
|
395 |
/2/jobs/[job_id] resource. |
|
396 |
|
|
397 |
It supports the following commands: GET, DELETE. |
|
398 |
|
|
399 |
GET |
|
400 |
~~~ |
|
401 |
|
|
402 |
:: |
|
403 |
|
|
404 |
Returns a job status. |
|
405 |
|
|
406 |
Returns: a dictionary with job parameters. |
|
407 |
The result includes: |
|
408 |
- id: job ID as a number |
|
409 |
- status: current job status as a string |
|
410 |
- ops: involved OpCodes as a list of dictionaries for each |
|
411 |
opcodes in the job |
|
412 |
- opstatus: OpCodes status as a list |
|
413 |
- opresult: OpCodes results as a list of lists |
|
414 |
|
|
415 |
DELETE |
|
416 |
~~~~~~ |
|
417 |
|
|
418 |
:: |
|
419 |
|
|
420 |
Cancel not-yet-started job. |
|
421 |
|
|
422 |
|
|
423 |
|
|
424 |
/2/nodes |
|
425 |
++++++++ |
|
426 |
|
|
427 |
:: |
|
428 |
|
|
429 |
/2/nodes resource. |
|
430 |
|
|
431 |
It supports the following commands: GET. |
|
432 |
|
|
433 |
GET |
|
434 |
~~~ |
|
435 |
|
|
436 |
:: |
|
437 |
|
|
438 |
Returns a list of all nodes. |
|
439 |
|
|
440 |
Example:: |
|
441 |
|
|
442 |
[ |
|
443 |
{ |
|
444 |
"id": "node1.example.com", |
|
445 |
"uri": "\/instances\/node1.example.com" |
|
446 |
}, |
|
447 |
{ |
|
448 |
"id": "node2.example.com", |
|
449 |
"uri": "\/instances\/node2.example.com" |
|
450 |
} |
|
451 |
] |
|
452 |
|
|
453 |
If the optional 'bulk' argument is provided and set to 'true' |
|
454 |
value (i.e '?bulk=1'), the output contains detailed |
|
455 |
information about nodes as a list. |
|
456 |
|
|
457 |
Example:: |
|
458 |
|
|
459 |
[ |
|
460 |
{ |
|
461 |
"pinst_cnt": 1, |
|
462 |
"mfree": 31280, |
|
463 |
"mtotal": 32763, |
|
464 |
"name": "www.example.com", |
|
465 |
"tags": [], |
|
466 |
"mnode": 512, |
|
467 |
"dtotal": 5246208, |
|
468 |
"sinst_cnt": 2, |
|
469 |
"dfree": 5171712, |
|
470 |
"offline": false |
|
471 |
}, |
|
472 |
... |
|
473 |
] |
|
474 |
|
|
475 |
Returns: a dictionary with 'name' and 'uri' keys for each of them |
|
476 |
|
|
477 |
/2/nodes/[node_name]/tags |
|
478 |
+++++++++++++++++++++++++ |
|
479 |
|
|
480 |
:: |
|
481 |
|
|
482 |
/2/nodes/[node_name]/tags resource. |
|
483 |
|
|
484 |
Manages per-node tags. |
|
485 |
|
|
486 |
It supports the following commands: GET, PUT, DELETE. |
|
487 |
|
|
488 |
GET |
|
489 |
~~~ |
|
490 |
|
|
491 |
:: |
|
492 |
|
|
493 |
Returns a list of tags. |
|
494 |
|
|
495 |
Example: ["tag1", "tag2", "tag3"] |
|
496 |
|
|
497 |
PUT |
|
498 |
~~~ |
|
499 |
|
|
500 |
:: |
|
501 |
|
|
502 |
Add a set of tags. |
|
503 |
|
|
504 |
The request as a list of strings should be PUT to this URI. And |
|
505 |
you'll have back a job id. |
|
506 |
|
|
507 |
DELETE |
|
508 |
~~~~~~ |
|
509 |
|
|
510 |
:: |
|
511 |
|
|
512 |
Delete a tag. |
|
513 |
|
|
514 |
In order to delete a set of tags, the DELETE |
|
515 |
request should be addressed to URI like: |
|
516 |
/tags?tag=[tag]&tag=[tag] |
|
517 |
|
|
518 |
/2/os |
|
519 |
+++++ |
|
520 |
|
|
521 |
:: |
|
522 |
|
|
523 |
/2/os resource. |
|
524 |
|
|
525 |
It supports the following commands: GET. |
|
526 |
|
|
527 |
GET |
|
528 |
~~~ |
|
529 |
|
|
530 |
:: |
|
531 |
|
|
532 |
Return a list of all OSes. |
|
533 |
|
|
534 |
Can return error 500 in case of a problem. |
|
535 |
|
|
536 |
Example: ["debian-etch"] |
|
537 |
|
|
538 |
/2/tags |
|
539 |
+++++++ |
|
540 |
|
|
541 |
:: |
|
542 |
|
|
543 |
/2/instances/tags resource. |
|
544 |
|
|
545 |
Manages cluster tags. |
|
546 |
|
|
547 |
It supports the following commands: GET, PUT, DELETE. |
|
548 |
|
|
549 |
GET |
|
550 |
~~~ |
|
551 |
|
|
552 |
:: |
|
553 |
|
|
554 |
Returns a list of tags. |
|
555 |
|
|
556 |
Example: ["tag1", "tag2", "tag3"] |
|
557 |
|
|
558 |
PUT |
|
559 |
~~~ |
|
560 |
|
|
561 |
:: |
|
562 |
|
|
563 |
Add a set of tags. |
|
564 |
|
|
565 |
The request as a list of strings should be PUT to this URI. And |
|
566 |
you'll have back a job id. |
|
567 |
|
|
568 |
DELETE |
|
569 |
~~~~~~ |
|
570 |
|
|
571 |
:: |
|
572 |
|
|
573 |
Delete a tag. |
|
574 |
|
|
575 |
In order to delete a set of tags, the DELETE |
|
576 |
request should be addressed to URI like: |
|
577 |
/tags?tag=[tag]&tag=[tag] |
|
578 |
|
|
579 |
/nodes/[node_name] |
|
580 |
++++++++++++++++++ |
|
581 |
|
|
582 |
:: |
|
583 |
|
|
584 |
/2/nodes/[node_name] resources. |
|
585 |
|
|
586 |
It supports the following commands: GET. |
|
587 |
|
|
588 |
GET |
|
589 |
~~~ |
|
590 |
|
|
591 |
:: |
|
592 |
|
|
593 |
Send information about a node. |
|
594 |
|
|
595 |
|
|
596 |
|
|
597 |
/version |
|
598 |
++++++++ |
|
599 |
|
|
600 |
:: |
|
601 |
|
|
602 |
/version resource. |
|
603 |
|
|
604 |
This resource should be used to determine the remote API version and |
|
605 |
to adapt clients accordingly. |
|
606 |
|
|
607 |
It supports the following commands: GET. |
|
608 |
|
|
609 |
GET |
|
610 |
~~~ |
|
611 |
|
|
612 |
:: |
|
613 |
|
|
614 |
Returns the remote API version. |
Also available in: Unified diff