499f9329af3aa6d309362f5167c5a0e1af772cba
[ganeti-local] / lib / rapi / rlib2.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010 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
22 """Remote API version 2 baserlib.library.
23
24   PUT or POST?
25   ============
26
27   According to RFC2616 the main difference between PUT and POST is that
28   POST can create new resources but PUT can only create the resource the
29   URI was pointing to on the PUT request.
30
31   To be in context of this module for instance creation POST on
32   /2/instances is legitim while PUT would be not, due to it does create a
33   new entity and not just replace /2/instances with it.
34
35   So when adding new methods, if they are operating on the URI entity itself,
36   PUT should be prefered over POST.
37
38 """
39
40 # pylint: disable-msg=C0103
41
42 # C0103: Invalid name, since the R_* names are not conforming
43
44 from ganeti import opcodes
45 from ganeti import http
46 from ganeti import constants
47 from ganeti import cli
48 from ganeti import utils
49 from ganeti import rapi
50 from ganeti.rapi import baserlib
51
52
53 _COMMON_FIELDS = ["ctime", "mtime", "uuid", "serial_no", "tags"]
54 I_FIELDS = ["name", "admin_state", "os",
55             "pnode", "snodes",
56             "disk_template",
57             "nic.ips", "nic.macs", "nic.modes", "nic.links", "nic.bridges",
58             "network_port",
59             "disk.sizes", "disk_usage",
60             "beparams", "hvparams",
61             "oper_state", "oper_ram", "oper_vcpus", "status",
62             "custom_hvparams", "custom_beparams", "custom_nicparams",
63             ] + _COMMON_FIELDS
64
65 N_FIELDS = ["name", "offline", "master_candidate", "drained",
66             "dtotal", "dfree",
67             "mtotal", "mnode", "mfree",
68             "pinst_cnt", "sinst_cnt",
69             "ctotal", "cnodes", "csockets",
70             "pip", "sip", "role",
71             "pinst_list", "sinst_list",
72             "master_capable", "vm_capable",
73             "group.uuid",
74             ] + _COMMON_FIELDS
75
76 G_FIELDS = ["name", "uuid",
77             "alloc_policy",
78             "node_cnt", "node_list",
79             "ctime", "mtime", "serial_no",
80             ]  # "tags" is missing to be able to use _COMMON_FIELDS here.
81
82 _NR_DRAINED = "drained"
83 _NR_MASTER_CANDIATE = "master-candidate"
84 _NR_MASTER = "master"
85 _NR_OFFLINE = "offline"
86 _NR_REGULAR = "regular"
87
88 _NR_MAP = {
89   "M": _NR_MASTER,
90   "C": _NR_MASTER_CANDIATE,
91   "D": _NR_DRAINED,
92   "O": _NR_OFFLINE,
93   "R": _NR_REGULAR,
94   }
95
96 # Request data version field
97 _REQ_DATA_VERSION = "__version__"
98
99 # Feature string for instance creation request data version 1
100 _INST_CREATE_REQV1 = "instance-create-reqv1"
101
102 # Feature string for instance reinstall request version 1
103 _INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
104
105 # Timeout for /2/jobs/[job_id]/wait. Gives job up to 10 seconds to change.
106 _WFJC_TIMEOUT = 10
107
108
109 class R_version(baserlib.R_Generic):
110   """/version resource.
111
112   This resource should be used to determine the remote API version and
113   to adapt clients accordingly.
114
115   """
116   @staticmethod
117   def GET():
118     """Returns the remote API version.
119
120     """
121     return constants.RAPI_VERSION
122
123
124 class R_2_info(baserlib.R_Generic):
125   """/2/info resource.
126
127   """
128   @staticmethod
129   def GET():
130     """Returns cluster information.
131
132     """
133     client = baserlib.GetClient()
134     return client.QueryClusterInfo()
135
136
137 class R_2_features(baserlib.R_Generic):
138   """/2/features resource.
139
140   """
141   @staticmethod
142   def GET():
143     """Returns list of optional RAPI features implemented.
144
145     """
146     return [_INST_CREATE_REQV1, _INST_REINSTALL_REQV1]
147
148
149 class R_2_os(baserlib.R_Generic):
150   """/2/os resource.
151
152   """
153   @staticmethod
154   def GET():
155     """Return a list of all OSes.
156
157     Can return error 500 in case of a problem.
158
159     Example: ["debian-etch"]
160
161     """
162     cl = baserlib.GetClient()
163     op = opcodes.OpDiagnoseOS(output_fields=["name", "variants"], names=[])
164     job_id = baserlib.SubmitJob([op], cl)
165     # we use custom feedback function, instead of print we log the status
166     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
167     diagnose_data = result[0]
168
169     if not isinstance(diagnose_data, list):
170       raise http.HttpBadGateway(message="Can't get OS list")
171
172     os_names = []
173     for (name, variants) in diagnose_data:
174       os_names.extend(cli.CalculateOSNames(name, variants))
175
176     return os_names
177
178
179 class R_2_redist_config(baserlib.R_Generic):
180   """/2/redistribute-config resource.
181
182   """
183   @staticmethod
184   def PUT():
185     """Redistribute configuration to all nodes.
186
187     """
188     return baserlib.SubmitJob([opcodes.OpRedistributeConfig()])
189
190
191 class R_2_cluster_modify(baserlib.R_Generic):
192   """/2/modify resource.
193
194   """
195   def PUT(self):
196     """Modifies cluster parameters.
197
198     @return: a job id
199
200     """
201     op = baserlib.FillOpcode(opcodes.OpSetClusterParams, self.request_body,
202                              None)
203
204     return baserlib.SubmitJob([op])
205
206
207 class R_2_jobs(baserlib.R_Generic):
208   """/2/jobs resource.
209
210   """
211   @staticmethod
212   def GET():
213     """Returns a dictionary of jobs.
214
215     @return: a dictionary with jobs id and uri.
216
217     """
218     fields = ["id"]
219     cl = baserlib.GetClient()
220     # Convert the list of lists to the list of ids
221     result = [job_id for [job_id] in cl.QueryJobs(None, fields)]
222     return baserlib.BuildUriList(result, "/2/jobs/%s",
223                                  uri_fields=("id", "uri"))
224
225
226 class R_2_jobs_id(baserlib.R_Generic):
227   """/2/jobs/[job_id] resource.
228
229   """
230   def GET(self):
231     """Returns a job status.
232
233     @return: a dictionary with job parameters.
234         The result includes:
235             - id: job ID as a number
236             - status: current job status as a string
237             - ops: involved OpCodes as a list of dictionaries for each
238               opcodes in the job
239             - opstatus: OpCodes status as a list
240             - opresult: OpCodes results as a list of lists
241
242     """
243     fields = ["id", "ops", "status", "summary",
244               "opstatus", "opresult", "oplog",
245               "received_ts", "start_ts", "end_ts",
246               ]
247     job_id = self.items[0]
248     result = baserlib.GetClient().QueryJobs([job_id, ], fields)[0]
249     if result is None:
250       raise http.HttpNotFound()
251     return baserlib.MapFields(fields, result)
252
253   def DELETE(self):
254     """Cancel not-yet-started job.
255
256     """
257     job_id = self.items[0]
258     result = baserlib.GetClient().CancelJob(job_id)
259     return result
260
261
262 class R_2_jobs_id_wait(baserlib.R_Generic):
263   """/2/jobs/[job_id]/wait resource.
264
265   """
266   # WaitForJobChange provides access to sensitive information and blocks
267   # machine resources (it's a blocking RAPI call), hence restricting access.
268   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
269
270   def GET(self):
271     """Waits for job changes.
272
273     """
274     job_id = self.items[0]
275
276     fields = self.getBodyParameter("fields")
277     prev_job_info = self.getBodyParameter("previous_job_info", None)
278     prev_log_serial = self.getBodyParameter("previous_log_serial", None)
279
280     if not isinstance(fields, list):
281       raise http.HttpBadRequest("The 'fields' parameter should be a list")
282
283     if not (prev_job_info is None or isinstance(prev_job_info, list)):
284       raise http.HttpBadRequest("The 'previous_job_info' parameter should"
285                                 " be a list")
286
287     if not (prev_log_serial is None or
288             isinstance(prev_log_serial, (int, long))):
289       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
290                                 " be a number")
291
292     client = baserlib.GetClient()
293     result = client.WaitForJobChangeOnce(job_id, fields,
294                                          prev_job_info, prev_log_serial,
295                                          timeout=_WFJC_TIMEOUT)
296     if not result:
297       raise http.HttpNotFound()
298
299     if result == constants.JOB_NOTCHANGED:
300       # No changes
301       return None
302
303     (job_info, log_entries) = result
304
305     return {
306       "job_info": job_info,
307       "log_entries": log_entries,
308       }
309
310
311 class R_2_nodes(baserlib.R_Generic):
312   """/2/nodes resource.
313
314   """
315   def GET(self):
316     """Returns a list of all nodes.
317
318     """
319     client = baserlib.GetClient()
320
321     if self.useBulk():
322       bulkdata = client.QueryNodes([], N_FIELDS, False)
323       return baserlib.MapBulkFields(bulkdata, N_FIELDS)
324     else:
325       nodesdata = client.QueryNodes([], ["name"], False)
326       nodeslist = [row[0] for row in nodesdata]
327       return baserlib.BuildUriList(nodeslist, "/2/nodes/%s",
328                                    uri_fields=("id", "uri"))
329
330
331 class R_2_nodes_name(baserlib.R_Generic):
332   """/2/nodes/[node_name] resource.
333
334   """
335   def GET(self):
336     """Send information about a node.
337
338     """
339     node_name = self.items[0]
340     client = baserlib.GetClient()
341
342     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
343                                             names=[node_name], fields=N_FIELDS,
344                                             use_locking=self.useLocking())
345
346     return baserlib.MapFields(N_FIELDS, result[0])
347
348
349 class R_2_nodes_name_role(baserlib.R_Generic):
350   """ /2/nodes/[node_name]/role resource.
351
352   """
353   def GET(self):
354     """Returns the current node role.
355
356     @return: Node role
357
358     """
359     node_name = self.items[0]
360     client = baserlib.GetClient()
361     result = client.QueryNodes(names=[node_name], fields=["role"],
362                                use_locking=self.useLocking())
363
364     return _NR_MAP[result[0][0]]
365
366   def PUT(self):
367     """Sets the node role.
368
369     @return: a job id
370
371     """
372     if not isinstance(self.request_body, basestring):
373       raise http.HttpBadRequest("Invalid body contents, not a string")
374
375     node_name = self.items[0]
376     role = self.request_body
377
378     if role == _NR_REGULAR:
379       candidate = False
380       offline = False
381       drained = False
382
383     elif role == _NR_MASTER_CANDIATE:
384       candidate = True
385       offline = drained = None
386
387     elif role == _NR_DRAINED:
388       drained = True
389       candidate = offline = None
390
391     elif role == _NR_OFFLINE:
392       offline = True
393       candidate = drained = None
394
395     else:
396       raise http.HttpBadRequest("Can't set '%s' role" % role)
397
398     op = opcodes.OpSetNodeParams(node_name=node_name,
399                                  master_candidate=candidate,
400                                  offline=offline,
401                                  drained=drained,
402                                  force=bool(self.useForce()))
403
404     return baserlib.SubmitJob([op])
405
406
407 class R_2_nodes_name_evacuate(baserlib.R_Generic):
408   """/2/nodes/[node_name]/evacuate resource.
409
410   """
411   def POST(self):
412     """Evacuate all secondary instances off a node.
413
414     """
415     node_name = self.items[0]
416     remote_node = self._checkStringVariable("remote_node", default=None)
417     iallocator = self._checkStringVariable("iallocator", default=None)
418     early_r = bool(self._checkIntVariable("early_release", default=0))
419     dry_run = bool(self.dryRun())
420
421     cl = baserlib.GetClient()
422
423     op = opcodes.OpNodeEvacuationStrategy(nodes=[node_name],
424                                           iallocator=iallocator,
425                                           remote_node=remote_node)
426
427     job_id = baserlib.SubmitJob([op], cl)
428     # we use custom feedback function, instead of print we log the status
429     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
430
431     jobs = []
432     for iname, node in result:
433       if dry_run:
434         jid = None
435       else:
436         op = opcodes.OpReplaceDisks(instance_name=iname,
437                                     remote_node=node, disks=[],
438                                     mode=constants.REPLACE_DISK_CHG,
439                                     early_release=early_r)
440         jid = baserlib.SubmitJob([op])
441       jobs.append((jid, iname, node))
442
443     return jobs
444
445
446 class R_2_nodes_name_migrate(baserlib.R_Generic):
447   """/2/nodes/[node_name]/migrate resource.
448
449   """
450   def POST(self):
451     """Migrate all primary instances from a node.
452
453     """
454     node_name = self.items[0]
455
456     if "live" in self.queryargs and "mode" in self.queryargs:
457       raise http.HttpBadRequest("Only one of 'live' and 'mode' should"
458                                 " be passed")
459     elif "live" in self.queryargs:
460       if self._checkIntVariable("live", default=1):
461         mode = constants.HT_MIGRATION_LIVE
462       else:
463         mode = constants.HT_MIGRATION_NONLIVE
464     else:
465       mode = self._checkStringVariable("mode", default=None)
466
467     op = opcodes.OpMigrateNode(node_name=node_name, mode=mode)
468
469     return baserlib.SubmitJob([op])
470
471
472 class R_2_nodes_name_storage(baserlib.R_Generic):
473   """/2/nodes/[node_name]/storage resource.
474
475   """
476   # LUQueryNodeStorage acquires locks, hence restricting access to GET
477   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
478
479   def GET(self):
480     node_name = self.items[0]
481
482     storage_type = self._checkStringVariable("storage_type", None)
483     if not storage_type:
484       raise http.HttpBadRequest("Missing the required 'storage_type'"
485                                 " parameter")
486
487     output_fields = self._checkStringVariable("output_fields", None)
488     if not output_fields:
489       raise http.HttpBadRequest("Missing the required 'output_fields'"
490                                 " parameter")
491
492     op = opcodes.OpQueryNodeStorage(nodes=[node_name],
493                                     storage_type=storage_type,
494                                     output_fields=output_fields.split(","))
495     return baserlib.SubmitJob([op])
496
497
498 class R_2_nodes_name_storage_modify(baserlib.R_Generic):
499   """/2/nodes/[node_name]/storage/modify resource.
500
501   """
502   def PUT(self):
503     node_name = self.items[0]
504
505     storage_type = self._checkStringVariable("storage_type", None)
506     if not storage_type:
507       raise http.HttpBadRequest("Missing the required 'storage_type'"
508                                 " parameter")
509
510     name = self._checkStringVariable("name", None)
511     if not name:
512       raise http.HttpBadRequest("Missing the required 'name'"
513                                 " parameter")
514
515     changes = {}
516
517     if "allocatable" in self.queryargs:
518       changes[constants.SF_ALLOCATABLE] = \
519         bool(self._checkIntVariable("allocatable", default=1))
520
521     op = opcodes.OpModifyNodeStorage(node_name=node_name,
522                                      storage_type=storage_type,
523                                      name=name,
524                                      changes=changes)
525     return baserlib.SubmitJob([op])
526
527
528 class R_2_nodes_name_storage_repair(baserlib.R_Generic):
529   """/2/nodes/[node_name]/storage/repair resource.
530
531   """
532   def PUT(self):
533     node_name = self.items[0]
534
535     storage_type = self._checkStringVariable("storage_type", None)
536     if not storage_type:
537       raise http.HttpBadRequest("Missing the required 'storage_type'"
538                                 " parameter")
539
540     name = self._checkStringVariable("name", None)
541     if not name:
542       raise http.HttpBadRequest("Missing the required 'name'"
543                                 " parameter")
544
545     op = opcodes.OpRepairNodeStorage(node_name=node_name,
546                                      storage_type=storage_type,
547                                      name=name)
548     return baserlib.SubmitJob([op])
549
550
551 def _ParseCreateGroupRequest(data, dry_run):
552   """Parses a request for creating a node group.
553
554   @rtype: L{opcodes.OpAddGroup}
555   @return: Group creation opcode
556
557   """
558   group_name = baserlib.CheckParameter(data, "name")
559   alloc_policy = baserlib.CheckParameter(data, "alloc_policy", default=None)
560
561   return opcodes.OpAddGroup(group_name=group_name,
562                             alloc_policy=alloc_policy,
563                             dry_run=dry_run)
564
565
566 class R_2_groups(baserlib.R_Generic):
567   """/2/groups resource.
568
569   """
570   def GET(self):
571     """Returns a list of all node groups.
572
573     """
574     client = baserlib.GetClient()
575
576     if self.useBulk():
577       bulkdata = client.QueryGroups([], G_FIELDS, False)
578       return baserlib.MapBulkFields(bulkdata, G_FIELDS)
579     else:
580       data = client.QueryGroups([], ["name"], False)
581       groupnames = [row[0] for row in data]
582       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
583                                    uri_fields=("name", "uri"))
584
585   def POST(self):
586     """Create a node group.
587
588     @return: a job id
589
590     """
591     baserlib.CheckType(self.request_body, dict, "Body contents")
592     op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
593     return baserlib.SubmitJob([op])
594
595
596 class R_2_groups_name(baserlib.R_Generic):
597   """/2/groups/[group_name] resource.
598
599   """
600   def GET(self):
601     """Send information about a node group.
602
603     """
604     group_name = self.items[0]
605     client = baserlib.GetClient()
606
607     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
608                                             names=[group_name], fields=G_FIELDS,
609                                             use_locking=self.useLocking())
610
611     return baserlib.MapFields(G_FIELDS, result[0])
612
613   def DELETE(self):
614     """Delete a node group.
615
616     """
617     op = opcodes.OpRemoveGroup(group_name=self.items[0],
618                                dry_run=bool(self.dryRun()))
619
620     return baserlib.SubmitJob([op])
621
622
623 def _ParseModifyGroupRequest(name, data):
624   """Parses a request for modifying a node group.
625
626   @rtype: L{opcodes.OpSetGroupParams}
627   @return: Group modify opcode
628
629   """
630   alloc_policy = baserlib.CheckParameter(data, "alloc_policy", default=None)
631   return opcodes.OpSetGroupParams(group_name=name, alloc_policy=alloc_policy)
632
633
634 class R_2_groups_name_modify(baserlib.R_Generic):
635   """/2/groups/[group_name]/modify resource.
636
637   """
638   def PUT(self):
639     """Changes some parameters of node group.
640
641     @return: a job id
642
643     """
644     baserlib.CheckType(self.request_body, dict, "Body contents")
645
646     op = _ParseModifyGroupRequest(self.items[0], self.request_body)
647
648     return baserlib.SubmitJob([op])
649
650
651 def _ParseRenameGroupRequest(name, data, dry_run):
652   """Parses a request for renaming a node group.
653
654   @type name: string
655   @param name: name of the node group to rename
656   @type data: dict
657   @param data: the body received by the rename request
658   @type dry_run: bool
659   @param dry_run: whether to perform a dry run
660
661   @rtype: L{opcodes.OpRenameGroup}
662   @return: Node group rename opcode
663
664   """
665   old_name = name
666   new_name = baserlib.CheckParameter(data, "new_name")
667
668   return opcodes.OpRenameGroup(old_name=old_name, new_name=new_name,
669                                dry_run=dry_run)
670
671
672 class R_2_groups_name_rename(baserlib.R_Generic):
673   """/2/groups/[group_name]/rename resource.
674
675   """
676   def PUT(self):
677     """Changes the name of a node group.
678
679     @return: a job id
680
681     """
682     baserlib.CheckType(self.request_body, dict, "Body contents")
683     op = _ParseRenameGroupRequest(self.items[0], self.request_body,
684                                   self.dryRun())
685     return baserlib.SubmitJob([op])
686
687
688 class R_2_groups_name_assign_nodes(baserlib.R_Generic):
689   """/2/groups/[group_name]/assign-nodes resource.
690
691   """
692   def PUT(self):
693     """Assigns nodes to a group.
694
695     @return: a job id
696
697     """
698     op = baserlib.FillOpcode(opcodes.OpAssignGroupNodes, self.request_body, {
699       "group_name": self.items[0],
700       "dry_run": self.dryRun(),
701       "force": self.useForce(),
702       })
703
704     return baserlib.SubmitJob([op])
705
706
707 def _ParseInstanceCreateRequestVersion1(data, dry_run):
708   """Parses an instance creation request version 1.
709
710   @rtype: L{opcodes.OpCreateInstance}
711   @return: Instance creation opcode
712
713   """
714   # Disks
715   disks_input = baserlib.CheckParameter(data, "disks", exptype=list)
716
717   disks = []
718   for idx, i in enumerate(disks_input):
719     baserlib.CheckType(i, dict, "Disk %d specification" % idx)
720
721     # Size is mandatory
722     try:
723       size = i[constants.IDISK_SIZE]
724     except KeyError:
725       raise http.HttpBadRequest("Disk %d specification wrong: missing disk"
726                                 " size" % idx)
727
728     disk = {
729       constants.IDISK_SIZE: size,
730       }
731
732     # Optional disk access mode
733     try:
734       disk_access = i[constants.IDISK_MODE]
735     except KeyError:
736       pass
737     else:
738       disk[constants.IDISK_MODE] = disk_access
739
740     disks.append(disk)
741
742   assert len(disks_input) == len(disks)
743
744   # Network interfaces
745   nics_input = baserlib.CheckParameter(data, "nics", exptype=list)
746
747   nics = []
748   for idx, i in enumerate(nics_input):
749     baserlib.CheckType(i, dict, "NIC %d specification" % idx)
750
751     nic = {}
752
753     for field in constants.INIC_PARAMS:
754       try:
755         value = i[field]
756       except KeyError:
757         continue
758
759       nic[field] = value
760
761     nics.append(nic)
762
763   assert len(nics_input) == len(nics)
764
765   # HV/BE parameters
766   hvparams = baserlib.CheckParameter(data, "hvparams", default={})
767   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
768
769   beparams = baserlib.CheckParameter(data, "beparams", default={})
770   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
771
772   return opcodes.OpCreateInstance(
773     mode=baserlib.CheckParameter(data, "mode"),
774     instance_name=baserlib.CheckParameter(data, "name"),
775     os_type=baserlib.CheckParameter(data, "os"),
776     osparams=baserlib.CheckParameter(data, "osparams", default={}),
777     force_variant=baserlib.CheckParameter(data, "force_variant",
778                                           default=False),
779     no_install=baserlib.CheckParameter(data, "no_install", default=False),
780     pnode=baserlib.CheckParameter(data, "pnode", default=None),
781     snode=baserlib.CheckParameter(data, "snode", default=None),
782     disk_template=baserlib.CheckParameter(data, "disk_template"),
783     disks=disks,
784     nics=nics,
785     src_node=baserlib.CheckParameter(data, "src_node", default=None),
786     src_path=baserlib.CheckParameter(data, "src_path", default=None),
787     start=baserlib.CheckParameter(data, "start", default=True),
788     wait_for_sync=True,
789     ip_check=baserlib.CheckParameter(data, "ip_check", default=True),
790     name_check=baserlib.CheckParameter(data, "name_check", default=True),
791     file_storage_dir=baserlib.CheckParameter(data, "file_storage_dir",
792                                              default=None),
793     file_driver=baserlib.CheckParameter(data, "file_driver",
794                                         default=constants.FD_LOOP),
795     source_handshake=baserlib.CheckParameter(data, "source_handshake",
796                                              default=None),
797     source_x509_ca=baserlib.CheckParameter(data, "source_x509_ca",
798                                            default=None),
799     source_instance_name=baserlib.CheckParameter(data, "source_instance_name",
800                                                  default=None),
801     iallocator=baserlib.CheckParameter(data, "iallocator", default=None),
802     hypervisor=baserlib.CheckParameter(data, "hypervisor", default=None),
803     hvparams=hvparams,
804     beparams=beparams,
805     dry_run=dry_run,
806     )
807
808
809 class R_2_instances(baserlib.R_Generic):
810   """/2/instances resource.
811
812   """
813   def GET(self):
814     """Returns a list of all available instances.
815
816     """
817     client = baserlib.GetClient()
818
819     use_locking = self.useLocking()
820     if self.useBulk():
821       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
822       return baserlib.MapBulkFields(bulkdata, I_FIELDS)
823     else:
824       instancesdata = client.QueryInstances([], ["name"], use_locking)
825       instanceslist = [row[0] for row in instancesdata]
826       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
827                                    uri_fields=("id", "uri"))
828
829   def _ParseVersion0CreateRequest(self):
830     """Parses an instance creation request version 0.
831
832     Request data version 0 is deprecated and should not be used anymore.
833
834     @rtype: L{opcodes.OpCreateInstance}
835     @return: Instance creation opcode
836
837     """
838     # Do not modify anymore, request data version 0 is deprecated
839     beparams = baserlib.MakeParamsDict(self.request_body,
840                                        constants.BES_PARAMETERS)
841     hvparams = baserlib.MakeParamsDict(self.request_body,
842                                        constants.HVS_PARAMETERS)
843     fn = self.getBodyParameter
844
845     # disk processing
846     disk_data = fn('disks')
847     if not isinstance(disk_data, list):
848       raise http.HttpBadRequest("The 'disks' parameter should be a list")
849     disks = []
850     for idx, d in enumerate(disk_data):
851       if not isinstance(d, int):
852         raise http.HttpBadRequest("Disk %d specification wrong: should"
853                                   " be an integer" % idx)
854       disks.append({"size": d})
855
856     # nic processing (one nic only)
857     nics = [{"mac": fn("mac", constants.VALUE_AUTO)}]
858     if fn("ip", None) is not None:
859       nics[0]["ip"] = fn("ip")
860     if fn("mode", None) is not None:
861       nics[0]["mode"] = fn("mode")
862     if fn("link", None) is not None:
863       nics[0]["link"] = fn("link")
864     if fn("bridge", None) is not None:
865       nics[0]["bridge"] = fn("bridge")
866
867     # Do not modify anymore, request data version 0 is deprecated
868     return opcodes.OpCreateInstance(
869       mode=constants.INSTANCE_CREATE,
870       instance_name=fn('name'),
871       disks=disks,
872       disk_template=fn('disk_template'),
873       os_type=fn('os'),
874       pnode=fn('pnode', None),
875       snode=fn('snode', None),
876       iallocator=fn('iallocator', None),
877       nics=nics,
878       start=fn('start', True),
879       ip_check=fn('ip_check', True),
880       name_check=fn('name_check', True),
881       wait_for_sync=True,
882       hypervisor=fn('hypervisor', None),
883       hvparams=hvparams,
884       beparams=beparams,
885       file_storage_dir=fn('file_storage_dir', None),
886       file_driver=fn('file_driver', constants.FD_LOOP),
887       dry_run=bool(self.dryRun()),
888       )
889
890   def POST(self):
891     """Create an instance.
892
893     @return: a job id
894
895     """
896     if not isinstance(self.request_body, dict):
897       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
898
899     # Default to request data version 0
900     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
901
902     if data_version == 0:
903       op = self._ParseVersion0CreateRequest()
904     elif data_version == 1:
905       op = _ParseInstanceCreateRequestVersion1(self.request_body,
906                                                self.dryRun())
907     else:
908       raise http.HttpBadRequest("Unsupported request data version %s" %
909                                 data_version)
910
911     return baserlib.SubmitJob([op])
912
913
914 class R_2_instances_name(baserlib.R_Generic):
915   """/2/instances/[instance_name] resource.
916
917   """
918   def GET(self):
919     """Send information about an instance.
920
921     """
922     client = baserlib.GetClient()
923     instance_name = self.items[0]
924
925     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
926                                             names=[instance_name],
927                                             fields=I_FIELDS,
928                                             use_locking=self.useLocking())
929
930     return baserlib.MapFields(I_FIELDS, result[0])
931
932   def DELETE(self):
933     """Delete an instance.
934
935     """
936     op = opcodes.OpRemoveInstance(instance_name=self.items[0],
937                                   ignore_failures=False,
938                                   dry_run=bool(self.dryRun()))
939     return baserlib.SubmitJob([op])
940
941
942 class R_2_instances_name_info(baserlib.R_Generic):
943   """/2/instances/[instance_name]/info resource.
944
945   """
946   def GET(self):
947     """Request detailed instance information.
948
949     """
950     instance_name = self.items[0]
951     static = bool(self._checkIntVariable("static", default=0))
952
953     op = opcodes.OpQueryInstanceData(instances=[instance_name],
954                                      static=static)
955     return baserlib.SubmitJob([op])
956
957
958 class R_2_instances_name_reboot(baserlib.R_Generic):
959   """/2/instances/[instance_name]/reboot resource.
960
961   Implements an instance reboot.
962
963   """
964   def POST(self):
965     """Reboot an instance.
966
967     The URI takes type=[hard|soft|full] and
968     ignore_secondaries=[False|True] parameters.
969
970     """
971     instance_name = self.items[0]
972     reboot_type = self.queryargs.get('type',
973                                      [constants.INSTANCE_REBOOT_HARD])[0]
974     ignore_secondaries = bool(self._checkIntVariable('ignore_secondaries'))
975     op = opcodes.OpRebootInstance(instance_name=instance_name,
976                                   reboot_type=reboot_type,
977                                   ignore_secondaries=ignore_secondaries,
978                                   dry_run=bool(self.dryRun()))
979
980     return baserlib.SubmitJob([op])
981
982
983 class R_2_instances_name_startup(baserlib.R_Generic):
984   """/2/instances/[instance_name]/startup resource.
985
986   Implements an instance startup.
987
988   """
989   def PUT(self):
990     """Startup an instance.
991
992     The URI takes force=[False|True] parameter to start the instance
993     if even if secondary disks are failing.
994
995     """
996     instance_name = self.items[0]
997     force_startup = bool(self._checkIntVariable('force'))
998     op = opcodes.OpStartupInstance(instance_name=instance_name,
999                                    force=force_startup,
1000                                    dry_run=bool(self.dryRun()))
1001
1002     return baserlib.SubmitJob([op])
1003
1004
1005 class R_2_instances_name_shutdown(baserlib.R_Generic):
1006   """/2/instances/[instance_name]/shutdown resource.
1007
1008   Implements an instance shutdown.
1009
1010   """
1011   def PUT(self):
1012     """Shutdown an instance.
1013
1014     """
1015     instance_name = self.items[0]
1016     op = opcodes.OpShutdownInstance(instance_name=instance_name,
1017                                     dry_run=bool(self.dryRun()))
1018
1019     return baserlib.SubmitJob([op])
1020
1021
1022 def _ParseInstanceReinstallRequest(name, data):
1023   """Parses a request for reinstalling an instance.
1024
1025   """
1026   if not isinstance(data, dict):
1027     raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1028
1029   ostype = baserlib.CheckParameter(data, "os")
1030   start = baserlib.CheckParameter(data, "start", exptype=bool,
1031                                   default=True)
1032   osparams = baserlib.CheckParameter(data, "osparams", default=None)
1033
1034   ops = [
1035     opcodes.OpShutdownInstance(instance_name=name),
1036     opcodes.OpReinstallInstance(instance_name=name, os_type=ostype,
1037                                 osparams=osparams),
1038     ]
1039
1040   if start:
1041     ops.append(opcodes.OpStartupInstance(instance_name=name, force=False))
1042
1043   return ops
1044
1045
1046 class R_2_instances_name_reinstall(baserlib.R_Generic):
1047   """/2/instances/[instance_name]/reinstall resource.
1048
1049   Implements an instance reinstall.
1050
1051   """
1052   def POST(self):
1053     """Reinstall an instance.
1054
1055     The URI takes os=name and nostartup=[0|1] optional
1056     parameters. By default, the instance will be started
1057     automatically.
1058
1059     """
1060     if self.request_body:
1061       if self.queryargs:
1062         raise http.HttpBadRequest("Can't combine query and body parameters")
1063
1064       body = self.request_body
1065     else:
1066       if not self.queryargs:
1067         raise http.HttpBadRequest("Missing query parameters")
1068       # Legacy interface, do not modify/extend
1069       body = {
1070         "os": self._checkStringVariable("os"),
1071         "start": not self._checkIntVariable("nostartup"),
1072         }
1073
1074     ops = _ParseInstanceReinstallRequest(self.items[0], body)
1075
1076     return baserlib.SubmitJob(ops)
1077
1078
1079 class R_2_instances_name_replace_disks(baserlib.R_Generic):
1080   """/2/instances/[instance_name]/replace-disks resource.
1081
1082   """
1083   def POST(self):
1084     """Replaces disks on an instance.
1085
1086     """
1087     instance_name = self.items[0]
1088     remote_node = self._checkStringVariable("remote_node", default=None)
1089     mode = self._checkStringVariable("mode", default=None)
1090     raw_disks = self._checkStringVariable("disks", default=None)
1091     iallocator = self._checkStringVariable("iallocator", default=None)
1092
1093     if raw_disks:
1094       try:
1095         disks = [int(part) for part in raw_disks.split(",")]
1096       except ValueError, err:
1097         raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
1098     else:
1099       disks = []
1100
1101     op = opcodes.OpReplaceDisks(instance_name=instance_name,
1102                                 remote_node=remote_node,
1103                                 mode=mode,
1104                                 disks=disks,
1105                                 iallocator=iallocator)
1106
1107     return baserlib.SubmitJob([op])
1108
1109
1110 class R_2_instances_name_activate_disks(baserlib.R_Generic):
1111   """/2/instances/[instance_name]/activate-disks resource.
1112
1113   """
1114   def PUT(self):
1115     """Activate disks for an instance.
1116
1117     The URI might contain ignore_size to ignore current recorded size.
1118
1119     """
1120     instance_name = self.items[0]
1121     ignore_size = bool(self._checkIntVariable('ignore_size'))
1122
1123     op = opcodes.OpActivateInstanceDisks(instance_name=instance_name,
1124                                          ignore_size=ignore_size)
1125
1126     return baserlib.SubmitJob([op])
1127
1128
1129 class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
1130   """/2/instances/[instance_name]/deactivate-disks resource.
1131
1132   """
1133   def PUT(self):
1134     """Deactivate disks for an instance.
1135
1136     """
1137     instance_name = self.items[0]
1138
1139     op = opcodes.OpDeactivateInstanceDisks(instance_name=instance_name)
1140
1141     return baserlib.SubmitJob([op])
1142
1143
1144 class R_2_instances_name_prepare_export(baserlib.R_Generic):
1145   """/2/instances/[instance_name]/prepare-export resource.
1146
1147   """
1148   def PUT(self):
1149     """Prepares an export for an instance.
1150
1151     @return: a job id
1152
1153     """
1154     instance_name = self.items[0]
1155     mode = self._checkStringVariable("mode")
1156
1157     op = opcodes.OpPrepareExport(instance_name=instance_name,
1158                                  mode=mode)
1159
1160     return baserlib.SubmitJob([op])
1161
1162
1163 def _ParseExportInstanceRequest(name, data):
1164   """Parses a request for an instance export.
1165
1166   @rtype: L{opcodes.OpExportInstance}
1167   @return: Instance export opcode
1168
1169   """
1170   mode = baserlib.CheckParameter(data, "mode",
1171                                  default=constants.EXPORT_MODE_LOCAL)
1172   target_node = baserlib.CheckParameter(data, "destination")
1173   shutdown = baserlib.CheckParameter(data, "shutdown", exptype=bool)
1174   remove_instance = baserlib.CheckParameter(data, "remove_instance",
1175                                             exptype=bool, default=False)
1176   x509_key_name = baserlib.CheckParameter(data, "x509_key_name", default=None)
1177   destination_x509_ca = baserlib.CheckParameter(data, "destination_x509_ca",
1178                                                 default=None)
1179
1180   return opcodes.OpExportInstance(instance_name=name,
1181                                   mode=mode,
1182                                   target_node=target_node,
1183                                   shutdown=shutdown,
1184                                   remove_instance=remove_instance,
1185                                   x509_key_name=x509_key_name,
1186                                   destination_x509_ca=destination_x509_ca)
1187
1188
1189 class R_2_instances_name_export(baserlib.R_Generic):
1190   """/2/instances/[instance_name]/export resource.
1191
1192   """
1193   def PUT(self):
1194     """Exports an instance.
1195
1196     @return: a job id
1197
1198     """
1199     if not isinstance(self.request_body, dict):
1200       raise http.HttpBadRequest("Invalid body contents, not a dictionary")
1201
1202     op = _ParseExportInstanceRequest(self.items[0], self.request_body)
1203
1204     return baserlib.SubmitJob([op])
1205
1206
1207 def _ParseMigrateInstanceRequest(name, data):
1208   """Parses a request for an instance migration.
1209
1210   @rtype: L{opcodes.OpMigrateInstance}
1211   @return: Instance migration opcode
1212
1213   """
1214   mode = baserlib.CheckParameter(data, "mode", default=None)
1215   cleanup = baserlib.CheckParameter(data, "cleanup", exptype=bool,
1216                                     default=False)
1217
1218   return opcodes.OpMigrateInstance(instance_name=name, mode=mode,
1219                                    cleanup=cleanup)
1220
1221
1222 class R_2_instances_name_migrate(baserlib.R_Generic):
1223   """/2/instances/[instance_name]/migrate resource.
1224
1225   """
1226   def PUT(self):
1227     """Migrates an instance.
1228
1229     @return: a job id
1230
1231     """
1232     baserlib.CheckType(self.request_body, dict, "Body contents")
1233
1234     op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
1235
1236     return baserlib.SubmitJob([op])
1237
1238
1239 def _ParseRenameInstanceRequest(name, data):
1240   """Parses a request for renaming an instance.
1241
1242   @rtype: L{opcodes.OpRenameInstance}
1243   @return: Instance rename opcode
1244
1245   """
1246   new_name = baserlib.CheckParameter(data, "new_name")
1247   ip_check = baserlib.CheckParameter(data, "ip_check", default=True)
1248   name_check = baserlib.CheckParameter(data, "name_check", default=True)
1249
1250   return opcodes.OpRenameInstance(instance_name=name, new_name=new_name,
1251                                   name_check=name_check, ip_check=ip_check)
1252
1253
1254 class R_2_instances_name_rename(baserlib.R_Generic):
1255   """/2/instances/[instance_name]/rename resource.
1256
1257   """
1258   def PUT(self):
1259     """Changes the name of an instance.
1260
1261     @return: a job id
1262
1263     """
1264     baserlib.CheckType(self.request_body, dict, "Body contents")
1265
1266     op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
1267
1268     return baserlib.SubmitJob([op])
1269
1270
1271 def _ParseModifyInstanceRequest(name, data):
1272   """Parses a request for modifying an instance.
1273
1274   @rtype: L{opcodes.OpSetInstanceParams}
1275   @return: Instance modify opcode
1276
1277   """
1278   osparams = baserlib.CheckParameter(data, "osparams", default={})
1279   force = baserlib.CheckParameter(data, "force", default=False)
1280   nics = baserlib.CheckParameter(data, "nics", default=[])
1281   disks = baserlib.CheckParameter(data, "disks", default=[])
1282   disk_template = baserlib.CheckParameter(data, "disk_template", default=None)
1283   remote_node = baserlib.CheckParameter(data, "remote_node", default=None)
1284   os_name = baserlib.CheckParameter(data, "os_name", default=None)
1285   force_variant = baserlib.CheckParameter(data, "force_variant", default=False)
1286
1287   # HV/BE parameters
1288   hvparams = baserlib.CheckParameter(data, "hvparams", default={})
1289   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES,
1290                       allowed_values=[constants.VALUE_DEFAULT])
1291
1292   beparams = baserlib.CheckParameter(data, "beparams", default={})
1293   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES,
1294                       allowed_values=[constants.VALUE_DEFAULT])
1295
1296   return opcodes.OpSetInstanceParams(instance_name=name, hvparams=hvparams,
1297                                      beparams=beparams, osparams=osparams,
1298                                      force=force, nics=nics, disks=disks,
1299                                      disk_template=disk_template,
1300                                      remote_node=remote_node, os_name=os_name,
1301                                      force_variant=force_variant)
1302
1303
1304 class R_2_instances_name_modify(baserlib.R_Generic):
1305   """/2/instances/[instance_name]/modify resource.
1306
1307   """
1308   def PUT(self):
1309     """Changes some parameters of an instance.
1310
1311     @return: a job id
1312
1313     """
1314     baserlib.CheckType(self.request_body, dict, "Body contents")
1315
1316     op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
1317
1318     return baserlib.SubmitJob([op])
1319
1320
1321 class R_2_instances_name_disk_grow(baserlib.R_Generic):
1322   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
1323
1324   """
1325   def POST(self):
1326     """Increases the size of an instance disk.
1327
1328     @return: a job id
1329
1330     """
1331     op = baserlib.FillOpcode(opcodes.OpGrowDisk, self.request_body, {
1332       "instance_name": self.items[0],
1333       "disk": int(self.items[1]),
1334       })
1335
1336     return baserlib.SubmitJob([op])
1337
1338
1339 class _R_Tags(baserlib.R_Generic):
1340   """ Quasiclass for tagging resources
1341
1342   Manages tags. When inheriting this class you must define the
1343   TAG_LEVEL for it.
1344
1345   """
1346   TAG_LEVEL = None
1347
1348   def __init__(self, items, queryargs, req):
1349     """A tag resource constructor.
1350
1351     We have to override the default to sort out cluster naming case.
1352
1353     """
1354     baserlib.R_Generic.__init__(self, items, queryargs, req)
1355
1356     if self.TAG_LEVEL == constants.TAG_CLUSTER:
1357       self.name = None
1358     else:
1359       self.name = items[0]
1360
1361   def GET(self):
1362     """Returns a list of tags.
1363
1364     Example: ["tag1", "tag2", "tag3"]
1365
1366     """
1367     # pylint: disable-msg=W0212
1368     return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
1369
1370   def PUT(self):
1371     """Add a set of tags.
1372
1373     The request as a list of strings should be PUT to this URI. And
1374     you'll have back a job id.
1375
1376     """
1377     # pylint: disable-msg=W0212
1378     if 'tag' not in self.queryargs:
1379       raise http.HttpBadRequest("Please specify tag(s) to add using the"
1380                                 " the 'tag' parameter")
1381     return baserlib._Tags_PUT(self.TAG_LEVEL,
1382                               self.queryargs['tag'], name=self.name,
1383                               dry_run=bool(self.dryRun()))
1384
1385   def DELETE(self):
1386     """Delete a tag.
1387
1388     In order to delete a set of tags, the DELETE
1389     request should be addressed to URI like:
1390     /tags?tag=[tag]&tag=[tag]
1391
1392     """
1393     # pylint: disable-msg=W0212
1394     if 'tag' not in self.queryargs:
1395       # no we not gonna delete all tags
1396       raise http.HttpBadRequest("Cannot delete all tags - please specify"
1397                                 " tag(s) using the 'tag' parameter")
1398     return baserlib._Tags_DELETE(self.TAG_LEVEL,
1399                                  self.queryargs['tag'],
1400                                  name=self.name,
1401                                  dry_run=bool(self.dryRun()))
1402
1403
1404 class R_2_instances_name_tags(_R_Tags):
1405   """ /2/instances/[instance_name]/tags resource.
1406
1407   Manages per-instance tags.
1408
1409   """
1410   TAG_LEVEL = constants.TAG_INSTANCE
1411
1412
1413 class R_2_nodes_name_tags(_R_Tags):
1414   """ /2/nodes/[node_name]/tags resource.
1415
1416   Manages per-node tags.
1417
1418   """
1419   TAG_LEVEL = constants.TAG_NODE
1420
1421
1422 class R_2_tags(_R_Tags):
1423   """ /2/tags resource.
1424
1425   Manages cluster tags.
1426
1427   """
1428   TAG_LEVEL = constants.TAG_CLUSTER